【Haskell学習|実務向け】データ定義の品質を左右する「全単射」の原則

導入

皆さんのコードベースにおいて、「同じ情報を表現するのに複数の書き方が存在してしまう」という状況に悩まされたことはありませんか?例えば、ユーザーの有効状態を表現するのに「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)を積極的に活用してください。これこそが、全単射なデータ定義を実現するための最強の武器となります。

コメント

タイトルとURLをコピーしました