Rustを勉強していると「ADT」「直和型」「直積型」という言葉に出会います。
最初は「なんか数学っぽくて難しそう...」と思うかもしれませんが、実はシンプルな概念です。そして、Rustの強力な型システムの根幹を支えるめちゃくちゃ大事な考え方でもあります。
この記事では、これらの概念を具体例とともに解説します。
直積型(Product Type)= 「AかつB」
「複数の値を全部持つ」 型です。
Rustでの例
// struct は直積型
struct Person {
name: String, // かつ
age: u32, // かつ
active: bool,
}
// タプルも直積型
type Point = (i32, i32); // i32 かつ i32
なぜ「積」なの?
取りうる値の数が、各フィールドの値の数の 掛け算 になるからです。
// (bool, bool) の場合
// bool = 2通り(true, false)
// 2 × 2 = 4通り
let patterns: [(bool, bool); 4] = [
(true, true),
(true, false),
(false, true),
(false, false),
];
直和型(Sum Type)= 「AまたはB」
「複数の選択肢のうち、どれか一つだけを持つ」 型です。
Rustでの例
// enum は直和型
enum Result<T, E> {
Ok(T), // または
Err(E),
}
enum Option<T> {
Some(T), // または
None,
}
enum Shape {
Circle(f64), // または
Rectangle(f64, f64), // または
Triangle(f64, f64, f64),
}
なぜ「和」なの?
取りうる値の数が、各バリアントの値の数の 足し算 になるからです。
// Option<bool> の場合
// Some(true), Some(false), None
// 2 + 1 = 3通り
ADT(代数的データ型)= 直積 + 直和
ADT(Algebraic Data Type) は、直積と直和を組み合わせて作る型の総称です。
「代数的」と呼ばれるのは、型を足し算(和)と掛け算(積)で計算できるからです。
// 直和の中に直積がある例
enum Message {
Quit, // 値なし
Move { x: i32, y: i32 }, // 直積(構造体)
Write(String), // 単一の値
ChangeColor(u8, u8, u8), // 直積(タプル)
}
直和型の真価:「ありえない状態」を型で防ぐ
ここからが本題です。直和型の最大のメリットは、「ありえない状態を型レベルで表現不可能にする」 ことです。
悪い例:直積だけで状態を表現
struct Connection {
is_connected: bool,
socket: Option<TcpStream>,
error: Option<String>,
}
この設計には問題があります。
// ありえないはずの状態が表現できてしまう
let invalid = Connection {
is_connected: false, // 切断中なのに...
socket: Some(stream), // ソケットがある?
error: None,
};
// あるいは
let also_invalid = Connection {
is_connected: true, // 接続中なのに...
socket: None, // ソケットがない?
error: Some("...".into()),
};
フィールドの組み合わせによって、論理的にありえない状態が生まれてしまいます。
良い例:直和型で状態を表現
enum Connection {
Disconnected,
Connecting,
Connected(TcpStream),
Failed(String),
}
この設計なら、各状態で必要なデータだけを持つ ことが保証されます。
fn handle_connection(conn: Connection) {
match conn {
Connection::Disconnected => {
// ソケットがないことが型で保証されている
println!("接続されていません");
}
Connection::Connecting => {
println!("接続中...");
}
Connection::Connected(stream) => {
// ソケットが必ず存在することが型で保証されている
send_data(&stream);
}
Connection::Failed(error) => {
// エラー情報が必ず存在することが型で保証されている
eprintln!("接続失敗: {}", error);
}
}
}
もう一つの例:ユーザーの認証状態
悪い例
struct User {
is_logged_in: bool,
user_id: Option<u64>,
guest_session_id: Option<String>,
}
「ログイン済みなのにuser_idがNone」「ゲストなのにuser_idがある」といった矛盾が起こりえます。
良い例
enum User {
Guest { session_id: String },
LoggedIn { user_id: u64, username: String },
Admin { user_id: u64, permissions: Vec<String> },
}
各状態に必要なデータが明確で、矛盾が起きません。
Option と Result が強力な理由
Rustの Option<T> と Result<T, E> が強力なのは、まさにこの直和型の恩恵です。
// Option<T> は「値があるかないか」を直和で表現
enum Option<T> {
Some(T),
None,
}
// Result<T, E> は「成功か失敗か」を直和で表現
enum Result<T, E> {
Ok(T),
Err(E),
}
他の言語のように null や例外に頼らず、型システムで「値がないかもしれない」「失敗するかもしれない」を表現 できます。
そして match で全パターンを網羅しないとコンパイルエラーになるため、処理漏れを防げます。
まとめ
| 概念 | 意味 | Rustでの実現 | 値の数 |
|---|---|---|---|
| 直積型 | AかつB(全部持つ) |
struct, タプル |
掛け算 |
| 直和型 | AまたはB(一つだけ) | enum |
足し算 |
| ADT | 直積と直和の組み合わせ |
struct + enum
|
- |
直和型を使いこなすと、「ありえない状態をコンパイル時に弾く」 設計ができるようになります。
これはRustの大きな強みであり、「コンパイルが通れば安心」と言われる所以でもあります。
ぜひ自分のコードでも「この bool フラグ、enum にできないかな?」と考えてみてください!