導入
皆さんのコードベースにおいて、「同じ情報を表現するのに複数の書き方が存在してしまう」という状況に悩まされたことはありませんか?例えば、ユーザーの有効状態を表現するのに「nullチェック」と「booleanフラグ」の両方が混在しているようなケースです。このような状態は、状態の遷移やバリデーションを複雑にし、予期せぬバグの温床となります。本記事では、関数型プログラミングの視点から、データ定義を「全単射(Bijection)」に近づけることで、堅牢なドメインモデルを構築する方法を解説します。
基礎知識
数学における「全単射」とは、2つの集合の間で「漏れなく、重複なく」1対1の対応関係が成り立っている状態を指します。データ定義においてこれを適用するとは、「あるデータを構築する関数(コンストラクタ)」と「そのデータを分解・識別する関数(パターンマッチ)」が、情報の損失や重複を一切生まずに可逆的であることを意味します。データ定義が全単射であれば、変換処理において「例外ケース」や「不正な状態」を考慮する必要がなくなり、コードの複雑性が劇的に低下します。
実装/解決策
実務において全単射を維持するためには、以下の原則を守ることが鍵となります。
1. 不変なデータ構造を採用する: 状態を後から変更可能にすると、データ構築後の整合性チェックが困難になります。
2. スマートコンストラクタを活用する: 外部からの不正なデータ入力を防ぎ、定義した型の中では常に「正しい状態」が保証されるようにします。
3. 網羅的なパターンマッチ: データの分解時には、コンパイラが全てのケースを網羅していることをチェックできるようにします。
サンプルプログラム
以下は、TypeScriptにおける「有効な期間」を表現する例です。開始日と終了日の関係性を全単射として定義することで、不正な期間(開始日が終了日より後など)を型レベルで排除します。
// 有効期間を表す型(スマートコンストラクタで不整合を防ぐ)
type ValidPeriod = {
readonly start: Date;
readonly end: Date;
private readonly _tag: 'ValidPeriod';
};
// データを構築する関数(全単射の「射」)
// 不正な場合は例外を投げるか、Option型を返すことで「不正な状態」を作らせない
export const createPeriod = (start: Date, end: Date): ValidPeriod => {
if (start > end) {
throw new Error("開始日は終了日より前である必要があります");
}
return { start, end, _tag: 'ValidPeriod' };
};
// データを分解する関数(全単射の「逆射」の役割)
// パターンマッチにより、常に内部データに安全にアクセスできる
export const getDurationDays = (period: ValidPeriod): number => {
const diff = period.end.getTime() - period.start.getTime();
return Math.floor(diff / (1000 60 60 24));
};
// 使用例
const period = createPeriod(new Date('2023-01-01'), new Date('2023-01-10'));
console.log(getDurationDays(period)); // 9日と出力される
応用・注意点
現場で陥りやすいのは、「バリデーションを型の外側で行い続けてしまうこと」です。APIのレスポンスを受け取った瞬間に、そのデータを「ドメインモデル」へ変換する境界(Boundary)を設けましょう。そこを通過したデータは全単射が保証されている、という前提を作ることで、その後のビジネスロジック内ではバリデーションを排除でき、テストコードも簡潔になります。また、プログラミング言語がサポートしている場合は、代数的データ型(ADT)を積極的に活用してください。これこそが、全単射なデータ定義を実現するための最強の武器となります。

コメント