代数的データ型
直和型と直積型を組み合わせてデータを表現する型システムの概念
型システム関数型プログラミング
代数的データ型とは
代数的データ型 (Algebraic Data Type, ADT) は、直和型 (Sum Type) と直積型 (Product Type) を組み合わせてデータを表現する型システムの概念である。Rust の enum、TypeScript の Discriminated Union が直和型に相当する。
直積型 (Product Type)
「A かつ B」。全てのフィールドを持つ。
// TypeScript: オブジェクト型 = 直積型
type User = { name: string; age: number };
// name と age の両方を持つ
// 取りうる値の数 = string の数 × number の数
直和型 (Sum Type)
「A または B」。いずれか 1 つの形態を取る。
// TypeScript: Discriminated Union = 直和型
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rectangle'; width: number; height: number }
| { kind: 'triangle'; base: number; height: number };
// circle, rectangle, triangle のいずれか 1 つ
直積型と直和型の対比
2 つの型は「組み合わせ方」が正反対であり、そこから値の数え方も網羅性チェックの効き方も分かれる。
| 観点 | 直積型 (Product Type) | 直和型 (Sum Type) |
|---|---|---|
| 組み合わせ方 | A かつ B | A または B |
| 値の持ち方 | 全てのフィールドを同時に持つ | 定義した形態のうち 1 つだけを取る |
| 取りうる値の数 | 各フィールドの数の掛け算 | 各形態の数の足し算 |
| 代表的な言語機能 | TypeScript のオブジェクト型 | TypeScript の Discriminated Union、Rust の enum |
| 上の例 | type User は name と age の両方を持つ | type Shape は circle / rectangle / triangle のいずれか 1 つ |
| 網羅性チェック | 効かない | 取りうる形態を型に列挙するため効く |
パターンマッチ
下の area は shape.kind で分岐し、各分岐の中では shape がその形態の型に絞り込まれる。
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle': return Math.PI * shape.radius ** 2;
case 'rectangle': return shape.width * shape.height;
case 'triangle': return shape.base * shape.height / 2;
}
}
// TypeScript が網羅性をチェック (全ケースを処理しないとエラー)
Rust の enum (ADT)
Rust の enum は、形態ごとに異なるデータ (タプルの形や名前つきの項目) を持たせられる。
enum Shape {
Circle(f64),
Rectangle(f64, f64),
Triangle { base: f64, height: f64 },
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rectangle(w, h) => w * h,
Shape::Triangle { base, height } => base * height / 2.0,
}
}
実用例: API レスポンス
読み込み中 / 失敗 / 成功の 3 つの状態を 1 つの型にまとめ、data は成功の時にだけ参照できる形にしている。
type ApiResult<T> =
| { status: 'success'; data: T }
| { status: 'error'; message: string; code: number }
| { status: 'loading' };
function renderResult(result: ApiResult<User>) {
switch (result.status) {
case 'loading': return <Spinner />;
case 'error': return <Error message={result.message} />;
case 'success': return <UserCard user={result.data} />;
}
}
なぜ重要か
代数的データ型を使うと、「ローディング中なのにデータがある」のような不正な状態を型レベルで排除できる。処理の漏れは、Rust の match なら既定で、TypeScript なら never 型への代入などの検査を書けば、コンパイル時に見つけられる。型定義は、取りうる状態の一覧を兼ねる。
代数的データ型の背景や設計思想は関連書籍に詳しい。
この記事は役に立ちましたか?