第7章: エラーハンドリング¶
この章で学ぶこと¶
- 関数型プログラミングにおけるエラー処理の考え方
- Option/Maybe型の実装と使い方
- Result/Either型の実装と使い方
- エラーの合成と連鎖
関数型エラーハンドリングの基本概念¶
関数型プログラミングでは、例外を投げる代わりにエラーを値として扱います。これにより:
- エラーが型システムで表現される
- エラー処理が明示的になる
- 関数の純粋性が保たれる
Option/Maybe型¶
Option型(Maybe型とも呼ばれる)は、値が存在するかしないかを表現します。
Option型の実装¶
// Option型の定義
type None = { type: "none" };
type Some<T> = { type: "some"; value: T };
type Option<T> = None | Some<T>;
// コンストラクタ関数
const none = (): None => ({ type: "none" });
const some = <T>(value: T): Some<T> => ({ type: "some", value });
// 型ガード
const isNone = <T>(option: Option<T>): option is None =>
option.type === "none";
const isSome = <T>(option: Option<T>): option is Some<T> =>
option.type === "some";
// 基本的な操作
const map = <T, U>(
option: Option<T>,
fn: (value: T) => U
): Option<U> => {
if (isNone(option)) return none();
return some(fn(option.value));
};
const flatMap = <T, U>(
option: Option<T>,
fn: (value: T) => Option<U>
): Option<U> => {
if (isNone(option)) return none();
return fn(option.value);
};
const getOrElse = <T>(
option: Option<T>,
defaultValue: T
): T => {
if (isNone(option)) return defaultValue;
return option.value;
};
Option型の使用例¶
// 安全な除算
const safeDivide = (a: number, b: number): Option<number> => {
if (b === 0) return none();
return some(a / b);
};
// 配列から要素を安全に取得
const getAt = <T>(array: T[], index: number): Option<T> => {
if (index < 0 || index >= array.length) return none();
return some(array[index]);
};
// Option値の連鎖
const calculateScore = (data: any): Option<number> => {
return flatMap(
getAt(data.scores, 0),
firstScore => flatMap(
getAt(data.scores, 1),
secondScore => map(
safeDivide(firstScore + secondScore, 2),
average => Math.round(average * 100)
)
)
);
};
Result/Either型¶
Result型(Either型とも呼ばれる)は、成功か失敗のいずれかを表現し、失敗時にはエラー情報を保持します。
Result型の実装¶
// Result型の定義
type Ok<T> = { type: "ok"; value: T };
type Err<E> = { type: "err"; error: E };
type Result<T, E> = Ok<T> | Err<E>;
// コンストラクタ関数
const ok = <T, E = never>(value: T): Result<T, E> =>
({ type: "ok", value });
const err = <T = never, E = unknown>(error: E): Result<T, E> =>
({ type: "err", error });
// 型ガード
const isOk = <T, E>(result: Result<T, E>): result is Ok<T> =>
result.type === "ok";
const isErr = <T, E>(result: Result<T, E>): result is Err<E> =>
result.type === "err";
// 基本的な操作
const mapResult = <T, U, E>(
result: Result<T, E>,
fn: (value: T) => U
): Result<U, E> => {
if (isErr(result)) return result;
return ok(fn(result.value));
};
const flatMapResult = <T, U, E>(
result: Result<T, E>,
fn: (value: T) => Result<U, E>
): Result<U, E> => {
if (isErr(result)) return result;
return fn(result.value);
};
const mapError = <T, E, F>(
result: Result<T, E>,
fn: (error: E) => F
): Result<T, F> => {
if (isOk(result)) return result;
return err(fn(result.error));
};
Result型の使用例¶
// エラーメッセージ付きのバリデーション
type ValidationError = {
field: string;
message: string;
};
const validateEmail = (email: string): Result<string, ValidationError> => {
if (!email.includes("@")) {
return err({
field: "email",
message: "Invalid email format"
});
}
return ok(email);
};
const validateAge = (age: number): Result<number, ValidationError> => {
if (age < 0 || age > 150) {
return err({
field: "age",
message: "Age must be between 0 and 150"
});
}
return ok(age);
};
// JSONのパース
const parseJSON = <T = unknown>(
json: string
): Result<T, Error> => {
try {
return ok(JSON.parse(json) as T);
} catch (error) {
return err(error as Error);
}
};
エラーの合成¶
複数のResult値を組み合わせる¶
// すべて成功した場合のみ成功を返す
const combineResults = <T, E>(
results: Result<T, E>[]
): Result<T[], E[]> => {
const values: T[] = [];
const errors: E[] = [];
for (const result of results) {
if (isOk(result)) {
values.push(result.value);
} else {
errors.push(result.error);
}
}
if (errors.length > 0) {
return err(errors);
}
return ok(values);
};
// 関数型のアプローチ
const sequence = <T, E>(
results: Result<T, E>[]
): Result<T[], E> => {
return results.reduce(
(acc: Result<T[], E>, result: Result<T, E>) =>
flatMapResult(acc, values =>
mapResult(result, value => [...values, value])
),
ok([])
);
};
バリデーションの組み合わせ¶
type User = {
name: string;
email: string;
age: number;
};
const validateName = (name: string): Result<string, ValidationError> => {
if (name.length < 2) {
return err({
field: "name",
message: "Name must be at least 2 characters"
});
}
return ok(name);
};
// ユーザー情報の総合バリデーション
const validateUser = (
name: string,
email: string,
age: number
): Result<User, ValidationError[]> => {
const results = [
validateName(name),
validateEmail(email),
validateAge(age)
];
const combined = combineResults(results);
if (isErr(combined)) {
return combined;
}
const [validName, validEmail, validAge] = combined.value;
return ok({
name: validName as string,
email: validEmail as string,
age: validAge as number
});
};
実践的なエラーハンドリングパターン¶
Railway Oriented Programming¶
// 処理の連鎖(Railway Pattern)
type Config = { apiKey: string; baseUrl: string };
type UserData = { id: number; name: string };
const loadConfig = (): Result<Config, string> => {
const apiKey = process.env.API_KEY;
const baseUrl = process.env.BASE_URL;
if (!apiKey || !baseUrl) {
return err("Missing configuration");
}
return ok({ apiKey, baseUrl });
};
const fetchUser = (
config: Config,
userId: number
): Promise<Result<UserData, string>> => {
return fetch(`${config.baseUrl}/users/${userId}`, {
headers: { "X-API-Key": config.apiKey }
})
.then(response => {
if (!response.ok) {
return err(`HTTP error: ${response.status}`);
}
return response.json();
})
.then(data => ok(data as UserData))
.catch(error => err(error.message));
};
// 処理の組み立て
const getUserName = async (userId: number): Promise<Result<string, string>> => {
const configResult = loadConfig();
if (isErr(configResult)) {
return configResult;
}
const userResult = await fetchUser(configResult.value, userId);
return mapResult(userResult, user => user.name);
};
エラーリカバリー¶
// エラー時の代替処理
const withDefault = <T, E>(
result: Result<T, E>,
defaultValue: T
): T => {
if (isOk(result)) return result.value;
return defaultValue;
};
// エラー時に別の処理を試行
const orElse = <T, E>(
result: Result<T, E>,
alternative: () => Result<T, E>
): Result<T, E> => {
if (isOk(result)) return result;
return alternative();
};
// リトライロジック
const retry = async <T, E>(
fn: () => Promise<Result<T, E>>,
maxAttempts: number,
delay: number = 1000
): Promise<Result<T, E>> => {
let lastError: E;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const result = await fn();
if (isOk(result)) {
return result;
}
lastError = result.error;
if (attempt < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, delay));
}
}
return err(lastError!);
};
実践演習¶
演習1: Option型のユーティリティ関数¶
以下の関数を実装してください:
// Option型の配列から、Some値のみを抽出
function catOptions<T>(options: Option<T>[]): T[] {
// 実装
}
// 条件に基づいてOption値を返す
function optionIf<T>(condition: boolean, value: () => T): Option<T> {
// 実装
}
解答
演習2: フォームバリデーション¶
Result型を使用して、フォームのバリデーションシステムを実装してください:
type FormData = {
username: string;
password: string;
confirmPassword: string;
email: string;
age: string;
};
type ValidatedForm = {
username: string;
password: string;
email: string;
age: number;
};
// この関数を実装
function validateForm(data: FormData): Result<ValidatedForm, ValidationError[]> {
// 実装
}
解答
const validateUsername = (username: string): Result<string, ValidationError> => {
if (username.length < 3) {
return err({
field: "username",
message: "Username must be at least 3 characters"
});
}
if (!/^[a-zA-Z0-9_]+$/.test(username)) {
return err({
field: "username",
message: "Username can only contain letters, numbers, and underscores"
});
}
return ok(username);
};
const validatePassword = (
password: string,
confirmPassword: string
): Result<string, ValidationError> => {
if (password.length < 8) {
return err({
field: "password",
message: "Password must be at least 8 characters"
});
}
if (password !== confirmPassword) {
return err({
field: "confirmPassword",
message: "Passwords do not match"
});
}
return ok(password);
};
const validateAgeString = (ageStr: string): Result<number, ValidationError> => {
const age = parseInt(ageStr, 10);
if (isNaN(age)) {
return err({
field: "age",
message: "Age must be a number"
});
}
return validateAge(age);
};
function validateForm(data: FormData): Result<ValidatedForm, ValidationError[]> {
const validations = [
validateUsername(data.username),
validatePassword(data.password, data.confirmPassword),
validateEmail(data.email),
validateAgeString(data.age)
];
const combined = combineResults(validations);
if (isErr(combined)) {
return combined;
}
const [username, password, email, age] = combined.value;
return ok({
username: username as string,
password: password as string,
email: email as string,
age: age as number
});
}
まとめ¶
この章では、関数型プログラミングにおけるエラーハンドリングについて学びました:
- ✅ Option型により、値の存在/不在を型安全に扱える
- ✅ Result型により、成功/失敗とエラー情報を明示的に扱える
- ✅ エラーを値として扱うことで、純粋関数を保ちながらエラー処理ができる
- ✅ 複数のエラーを合成し、包括的なエラーハンドリングが可能
次の章では、非同期処理を関数型のアプローチで扱う方法について学びます。