代数的データ型

Essential
最終更新: タグ: Rust, ADT

Rustで定義するすべての型は2つの操作から構成される:組み合わせ選択だ。この2つの操作を繰り返し適用することで、任意のデータ構造を記述できる。

この考え方には名前がある:代数的データ型(algebraic data types)、略してADTだ。

2つの操作

最初の操作は**直積(product)**だ:複数の値を1つに組み合わせる。2次元空間の点はx座標 かつ y座標だ。ユーザーレコードは名前 かつ 年齢 かつ メールアドレスだ。直積を持っていれば、常にそのすべての部分を持っている。Rustではこれを struct と書く。

2番目の操作は**直和(sum)**だ:固定された選択肢の集合から1つを選ぶ。信号機は赤 またはまたは 緑だ。パース結果は有効な値 または エラーだ。直和を持っていれば、常にちょうど1つのバリアント(variant)を持っている。Rustではこれを enum と書く。

Rustのすべてのユーザー定義型は、これら2つの操作の何らかの組み合わせだ。

なぜ「代数的」か

この用語は値の数を数えることから来ている。

直積型(product type) A×BA \times B はちょうど AB|A| \cdot |B| 個の可能な値を持つ — AA の値と BB の値の各組み合わせに1つ対応する。bool が2つの値を持ち u8 が256個の値を持つなら、両方を保持する構造体は 2×256=5122 \times 256 = 512 個の可能な値を持つ。

直和型(sum type) A+BA + B はちょうど A+B|A| + |B| 個の可能な値を持つ — AA の各値に1つ、BB の各値に1つだ。2つのユニットバリアントを持つ列挙型(enum)は 1+1=21 + 1 = 2 個の可能な値を持つ — ちょうど bool と同じだ。

この数え方の視点はデータモデルを設計するときに重要だ。型がドメインの必要より多くの可能な状態を持つ場合、コードは実行時に不可能な状態を防ぐガードが必要になる。適切な数の状態を持つ型を設計することで、コンパイラがそのガードを排除できる。

RustでADTが重要な理由

RustでのADTの恩恵は**網羅性(exhaustiveness)**だ。列挙型を match するとき、Rustはすべてのバリアントを処理することを要求する。コンパイル時にケースを忘れることはできない。

列挙型に新しいバリアントを追加すると、キャッチオール(catch-all)を持たないすべての match はコンパイルに失敗する — 組み込みのチェックリストとして機能する。ADTはドメインモデリングを正確さの強制という形式に変える。

直積と直和の組み合わせ

真の力は2つの操作を組み合わせることから来る。Result<T, E> は直和型だ:Ok(T) または Err(E)。内部の T はそれ自体が直積型 — 複数のフィールドを保持する構造体 — かもしれない。

enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
    Triangle { base: f64, height: f64 },
}

「and(かつ)」と「or(または)」で英語で記述できるデータモデルはすべて、ADTに直接マッピングされる。

Rustのカバレッジ

Rustの struct は直積型をカバーし、enum は直和型をカバーする。合わさると、ADTで表現可能なすべての空間をカバーする。選択可能性を表現するために継承階層、判別共用体のワークアラウンド、またはnullableフィールドは必要ない — すべてはハックなしで structenum で処理される。

次の2つのチェックポイント、直積型直和型で、それぞれの構文とイディオムを説明する。