Skilore

代数的データ型(Algebraic Data Types)

ADT は「直積型(AND)と直和型(OR)の組み合わせ」でデータを正確にモデリングする手法。不正な状態を型で表現不可能にする。

86 分で読めます42,545 文字

代数的データ型(Algebraic Data Types)

ADT は「直積型(AND)と直和型(OR)の組み合わせ」でデータを正確にモデリングする手法。不正な状態を型で表現不可能にする。

この章で学ぶこと

  • 直積型と直和型の概念を理解する
  • パターンマッチとの組み合わせを活用できる
  • 「不正な状態を表現不可能にする」設計ができる
  • Null 問題と Option/Maybe 型の意義を理解する
  • 実務でのドメインモデリングに ADT を活用できる
  • 再帰的データ型とジェネリック ADT を実装できる
  • 各言語の ADT サポートの違いを把握する

前提知識

このガイドを読む前に、以下の知識があると理解が深まります:


1. 直積型(Product Types)

直積型 = 「AかつB」(AND)
  → 全てのフィールドを同時に持つ

  構造体 / レコード / タプル / クラス

  なぜ「積」と呼ぶか:
    型 A が a 通り、型 B が b 通りの値を持つとき
    (A, B) は a × b 通りの値を持つ
    例: (Bool, Bool) = 2 × 2 = 4 通り
        (True, True), (True, False), (False, True), (False, False)

Rust

// Rust: 構造体(直積型)
struct User {
    name: String,      // AND
    age: u32,          // AND
    email: String,     // AND
}
// User = String × u32 × String
// 取りうる値の数 = name の値の数 × age の値の数 × email の値の数
 
// タプル(無名の直積型)
let point: (f64, f64) = (3.0, 4.0);
 
// タプル構造体(名前付きタプル)
struct Color(u8, u8, u8);
let red = Color(255, 0, 0);
println!("R: {}", red.0);
 
// ニュータイプパターン(単一フィールドのタプル構造体)
struct UserId(u64);
struct OrderId(u64);
// UserId と OrderId は異なる型 → 混同できない
 
fn get_user(id: UserId) -> Option<User> { /* ... */ }
fn get_order(id: OrderId) -> Option<Order> { /* ... */ }
 
let user_id = UserId(42);
let order_id = OrderId(42);
get_user(user_id);    // OK
// get_user(order_id); // コンパイルエラー: 型が違う
 
// ユニット構造体(フィールドなし)
struct Marker;
// サイズ 0。型レベルのマーカーとして使う
 
// 構造体の更新構文
struct Config {
    host: String,
    port: u16,
    max_connections: u32,
    timeout_ms: u64,
    debug: bool,
}
 
impl Config {
    fn default() -> Self {
        Config {
            host: "localhost".to_string(),
            port: 8080,
            max_connections: 100,
            timeout_ms: 5000,
            debug: false,
        }
    }
}
 
let config = Config {
    port: 3000,
    debug: true,
    ..Config::default()  // 残りのフィールドはデフォルト値
};

TypeScript

// TypeScript: インターフェース(直積型)
interface User {
    name: string;     // AND
    age: number;      // AND
    email: string;    // AND
}
 
// タプル型
type Point = [number, number];
type RGB = [r: number, g: number, b: number]; // ラベル付きタプル
 
// ブランド型(ニュータイプパターンの TypeScript 版)
type UserId = string & { readonly __brand: unique symbol };
type OrderId = string & { readonly __brand: unique symbol };
 
function createUserId(id: string): UserId {
    return id as UserId;
}
 
function createOrderId(id: string): OrderId {
    return id as OrderId;
}
 
function getUser(id: UserId): User | null { /* ... */ return null; }
function getOrder(id: OrderId): Order | null { /* ... */ return null; }
 
const userId = createUserId("u-123");
const orderId = createOrderId("o-456");
getUser(userId);    // OK
// getUser(orderId); // 型エラー
 
// レコード型
type Config = {
    readonly host: string;
    readonly port: number;
    readonly maxConnections: number;
    readonly timeoutMs: number;
    readonly debug: boolean;
};
 
// Readonly ユーティリティ型
type ReadonlyConfig = Readonly<Config>;
 
// 交差型による直積の合成
type HasId = { id: string };
type HasTimestamps = { createdAt: Date; updatedAt: Date };
type HasSoftDelete = { deletedAt: Date | null };
 
type Entity = HasId & HasTimestamps;
type SoftDeletableEntity = Entity & HasSoftDelete;

Haskell

-- Haskell: data 宣言による直積型
data User = User
    { userName  :: String
    , userAge   :: Int
    , userEmail :: String
    }
-- レコード構文で自動的にフィールドアクセサ関数が生成される
 
-- タプル
type Point = (Double, Double)
 
-- newtype(ゼロコストの型ラッパー)
newtype UserId = UserId Int deriving (Eq, Ord, Show)
newtype OrderId = OrderId Int deriving (Eq, Ord, Show)
-- newtype は実行時にはラップなし(コンパイル時のみの区別)

直積型の値の数と情報量

型の代数:

  Void(値なし)      = 0
  Unit / ()           = 1
  Bool                = 2
  u8 / byte           = 256
  (Bool, Bool)        = 2 × 2 = 4
  (Bool, u8)          = 2 × 256 = 512
  (u8, u8)            = 256 × 256 = 65,536
  (Bool, Bool, Bool)  = 2 × 2 × 2 = 8

  直積型の情報量(ビット数):
    log₂(a × b) = log₂(a) + log₂(b)
    つまり、フィールドを増やすと情報量が「加算」される

2. 直和型(Sum Types / Tagged Unions)

直和型 = 「AまたはB」(OR)
  → 複数の候補のうち1つだけを持つ

  列挙型 / ユニオン型 / バリアント

  なぜ「和」と呼ぶか:
    型 A が a 通り、型 B が b 通りの値を持つとき
    A | B は a + b 通りの値を持つ
    例: Bool | Unit = 2 + 1 = 3 通り
        True, False, ()

Rust

// Rust: enum(直和型の最も洗練された形)
enum Shape {
    Circle(f64),                    // 半径
    Rectangle(f64, f64),            // 幅, 高さ
    Triangle(f64, f64, f64),        // 3辺
}
// Shape = Circle(f64) + Rectangle(f64, f64) + Triangle(f64, f64, f64)
// いずれか1つだけ
 
// Option: 「値があるかないか」を型で表現
enum Option<T> {
    Some(T),
    None,
}
 
// Result: 「成功か失敗か」を型で表現
enum Result<T, E> {
    Ok(T),
    Err(E),
}
 
// 名前付きフィールドを持つバリアント
enum Event {
    Click { x: f64, y: f64, button: MouseButton },
    KeyPress { key: char, modifiers: Modifiers },
    Scroll { delta_x: f64, delta_y: f64 },
    Resize { width: u32, height: u32 },
    Close,
}
 
enum MouseButton {
    Left,
    Right,
    Middle,
}
 
struct Modifiers {
    shift: bool,
    ctrl: bool,
    alt: bool,
    meta: bool,
}
 
// 再帰的な直和型(木構造)
enum BinaryTree<T> {
    Leaf(T),
    Node {
        left: Box<BinaryTree<T>>,
        value: T,
        right: Box<BinaryTree<T>>,
    },
}
 
impl<T: Ord + Clone> BinaryTree<T> {
    fn insert(self, new_value: T) -> BinaryTree<T> {
        match self {
            BinaryTree::Leaf(v) => {
                if new_value < v {
                    BinaryTree::Node {
                        left: Box::new(BinaryTree::Leaf(new_value)),
                        value: v.clone(),
                        right: Box::new(BinaryTree::Leaf(v)),
                    }
                } else {
                    BinaryTree::Node {
                        left: Box::new(BinaryTree::Leaf(v.clone())),
                        value: v,
                        right: Box::new(BinaryTree::Leaf(new_value)),
                    }
                }
            }
            BinaryTree::Node { left, value, right } => {
                if new_value < value {
                    BinaryTree::Node {
                        left: Box::new(left.insert(new_value)),
                        value,
                        right,
                    }
                } else {
                    BinaryTree::Node {
                        left,
                        value,
                        right: Box::new(right.insert(new_value)),
                    }
                }
            }
        }
    }
}
 
// JSON 値の表現
enum JsonValue {
    Null,
    Bool(bool),
    Number(f64),
    Str(String),
    Array(Vec<JsonValue>),
    Object(HashMap<String, JsonValue>),
}

TypeScript

// TypeScript: ユニオン型 + 判別フィールド
type Shape =
    | { kind: "circle"; radius: number }
    | { kind: "rectangle"; width: number; height: number }
    | { kind: "triangle"; a: number; b: number; c: number };
 
// 判別ユニオン(Discriminated Union)
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": {
            const s = (shape.a + shape.b + shape.c) / 2;
            return Math.sqrt(s * (s-shape.a) * (s-shape.b) * (s-shape.c));
        }
    }
}
 
// イベント型の定義
type UIEvent =
    | { type: "click"; x: number; y: number; button: "left" | "right" | "middle" }
    | { type: "keypress"; key: string; modifiers: { shift: boolean; ctrl: boolean; alt: boolean } }
    | { type: "scroll"; deltaX: number; deltaY: number }
    | { type: "resize"; width: number; height: number }
    | { type: "close" };
 
function handleEvent(event: UIEvent): void {
    switch (event.type) {
        case "click":
            console.log(`Click at (${event.x}, ${event.y}) with ${event.button}`);
            break;
        case "keypress":
            console.log(`Key: ${event.key}`);
            break;
        case "scroll":
            console.log(`Scroll: (${event.deltaX}, ${event.deltaY})`);
            break;
        case "resize":
            console.log(`Resize to ${event.width}x${event.height}`);
            break;
        case "close":
            console.log("Window closed");
            break;
    }
}
 
// JSON 値の型定義
type JsonValue =
    | null
    | boolean
    | number
    | string
    | JsonValue[]
    | { [key: string]: JsonValue };
 
// 再帰的な式の木(AST)
type Expr =
    | { type: "literal"; value: number }
    | { type: "variable"; name: string }
    | { type: "binary"; op: "+" | "-" | "*" | "/"; left: Expr; right: Expr }
    | { type: "unary"; op: "-" | "!"; operand: Expr }
    | { type: "call"; name: string; args: Expr[] }
    | { type: "if"; condition: Expr; then: Expr; else: Expr };
 
function evaluate(expr: Expr, env: Record<string, number>): number {
    switch (expr.type) {
        case "literal":
            return expr.value;
        case "variable":
            if (!(expr.name in env)) throw new Error(`Undefined: ${expr.name}`);
            return env[expr.name];
        case "binary": {
            const left = evaluate(expr.left, env);
            const right = evaluate(expr.right, env);
            switch (expr.op) {
                case "+": return left + right;
                case "-": return left - right;
                case "*": return left * right;
                case "/": return left / right;
            }
        }
        case "unary": {
            const operand = evaluate(expr.operand, env);
            switch (expr.op) {
                case "-": return -operand;
                case "!": return operand === 0 ? 1 : 0;
            }
        }
        case "call":
            throw new Error("Function calls not implemented");
        case "if":
            return evaluate(expr.condition, env) !== 0
                ? evaluate(expr.then, env)
                : evaluate(expr.else, env);
    }
}

Haskell

-- Haskell: data 宣言(ADTの本家)
data Shape
    = Circle Double
    | Rectangle Double Double
    | Triangle Double Double Double
 
area :: Shape -> Double
area (Circle r)        = pi * r * r
area (Rectangle w h)   = w * h
area (Triangle a b c)  = let s = (a + b + c) / 2
                          in sqrt (s * (s-a) * (s-b) * (s-c))
 
-- 再帰的なデータ型
data List a = Nil | Cons a (List a)
  deriving (Show, Eq)
 
-- 使用例
myList :: List Int
myList = Cons 1 (Cons 2 (Cons 3 Nil))
 
-- リストの長さ
length' :: List a -> Int
length' Nil         = 0
length' (Cons _ xs) = 1 + length' xs
 
-- リストの畳み込み
foldList :: (a -> b -> b) -> b -> List a -> b
foldList _ acc Nil         = acc
foldList f acc (Cons x xs) = f x (foldList f acc xs)
 
-- 二分木
data Tree a
    = Empty
    | Branch (Tree a) a (Tree a)
  deriving (Show, Eq)
 
-- 木の挿入
insert :: (Ord a) => a -> Tree a -> Tree a
insert x Empty = Branch Empty x Empty
insert x (Branch left val right)
    | x < val   = Branch (insert x left) val right
    | x > val   = Branch left val (insert x right)
    | otherwise  = Branch left val right
 
-- 木の探索
search :: (Ord a) => a -> Tree a -> Bool
search _ Empty = False
search x (Branch left val right)
    | x == val  = True
    | x < val   = search x left
    | otherwise  = search x right
 
-- JSON 値
data JsonValue
    = JsonNull
    | JsonBool Bool
    | JsonNumber Double
    | JsonString String
    | JsonArray [JsonValue]
    | JsonObject [(String, JsonValue)]
  deriving (Show, Eq)
 
-- JSON の表示
showJson :: JsonValue -> String
showJson JsonNull        = "null"
showJson (JsonBool True) = "true"
showJson (JsonBool False) = "false"
showJson (JsonNumber n)  = show n
showJson (JsonString s)  = "\"" ++ s ++ "\""
showJson (JsonArray xs)  = "[" ++ intercalate ", " (map showJson xs) ++ "]"
showJson (JsonObject ps) = "{" ++ intercalate ", " (map showPair ps) ++ "}"
  where showPair (k, v) = "\"" ++ k ++ "\": " ++ showJson v
 
-- Either: 2つの型のどちらかを持つ
data Either a b = Left a | Right b
 
-- 慣例: Left はエラー、Right は成功
safeDivide :: Double -> Double -> Either String Double
safeDivide _ 0 = Left "Division by zero"
safeDivide x y = Right (x / y)

Go

// Go: インターフェースで直和型を模倣(sealed interface パターン)
type Shape interface {
    isShape()  // プライベートメソッドで外部からの実装を防止
    Area() float64
}
 
type Circle struct {
    Radius float64
}
 
type Rectangle struct {
    Width  float64
    Height float64
}
 
type Triangle struct {
    A, B, C float64
}
 
func (c Circle) isShape()    {}
func (r Rectangle) isShape() {}
func (t Triangle) isShape()  {}
 
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}
 
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}
 
func (t Triangle) Area() float64 {
    s := (t.A + t.B + t.C) / 2
    return math.Sqrt(s * (s - t.A) * (s - t.B) * (s - t.C))
}
 
// 型スイッチ(パターンマッチの代替)
func describe(s Shape) string {
    switch v := s.(type) {
    case Circle:
        return fmt.Sprintf("Circle(r=%.1f)", v.Radius)
    case Rectangle:
        return fmt.Sprintf("Rectangle(%.1f x %.1f)", v.Width, v.Height)
    case Triangle:
        return fmt.Sprintf("Triangle(%.1f, %.1f, %.1f)", v.A, v.B, v.C)
    default:
        return "Unknown shape"
    }
}

3. パターンマッチ

Rust の高度なパターンマッチ

// Rust: match による網羅的パターンマッチ
fn describe(shape: &Shape) -> String {
    match shape {
        Shape::Circle(r) => format!("Circle with radius {}", r),
        Shape::Rectangle(w, h) => format!("{}x{} rectangle", w, h),
        Shape::Triangle(a, b, c) => format!("Triangle ({}, {}, {})", a, b, c),
    }
    // 全バリアントを処理しないとコンパイルエラー(網羅性チェック)
}
 
// Option のパターンマッチ
fn greet(name: Option<&str>) -> String {
    match name {
        Some(n) => format!("Hello, {}!", n),
        None => "Hello, stranger!".to_string(),
    }
}
 
// if let(単一パターン)
if let Some(name) = get_name() {
    println!("Found: {}", name);
}
 
// while let(ループとパターンマッチの組み合わせ)
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
    println!("{}", top);
}
 
// let else(Rust 1.65+)
fn process_input(input: &str) -> Result<u32, String> {
    let Ok(number) = input.parse::<u32>() else {
        return Err(format!("Failed to parse: {}", input));
    };
    Ok(number * 2)
}
 
// ネストしたパターン
match result {
    Ok(Some(value)) if value > 0 => println!("Positive: {}", value),
    Ok(Some(value)) => println!("Non-positive: {}", value),
    Ok(None) => println!("No value"),
    Err(e) => println!("Error: {}", e),
}
 
// 構造体のパターンマッチ
struct Point { x: f64, y: f64 }
 
fn classify_point(p: &Point) -> &str {
    match p {
        Point { x: 0.0, y: 0.0 } => "origin",
        Point { x, y: 0.0 } => "on x-axis",
        Point { x: 0.0, y } => "on y-axis",
        Point { x, y } if x == y => "on diagonal",
        _ => "elsewhere",
    }
}
 
// 範囲パターン
fn classify_age(age: u32) -> &'static str {
    match age {
        0..=2 => "乳児",
        3..=5 => "幼児",
        6..=11 => "小学生",
        12..=14 => "中学生",
        15..=17 => "高校生",
        18..=64 => "成人",
        65.. => "高齢者",
    }
}
 
// OR パターン
fn is_vowel(c: char) -> bool {
    matches!(c, 'a' | 'e' | 'i' | 'o' | 'u' | 'A' | 'E' | 'I' | 'O' | 'U')
}
 
// バインディング(@ パターン)
fn classify_number(n: i32) -> String {
    match n {
        n @ 1..=9 => format!("small positive: {}", n),
        n @ 10..=99 => format!("medium positive: {}", n),
        n @ 100.. => format!("large positive: {}", n),
        0 => "zero".to_string(),
        n => format!("negative: {}", n),
    }
}
 
// スライスパターン
fn describe_slice(slice: &[i32]) -> String {
    match slice {
        [] => "empty".to_string(),
        [x] => format!("single: {}", x),
        [first, .., last] => format!("from {} to {}", first, last),
    }
}
 
// 参照パターン
fn process_references(values: &[Option<String>]) {
    for value in values {
        match value {
            Some(ref s) if s.starts_with("A") => println!("Starts with A: {}", s),
            Some(ref s) => println!("Other: {}", s),
            None => println!("Missing"),
        }
    }
}

TypeScript の網羅性チェック

// TypeScript: exhaustiveness check(網羅性チェック)
type Shape =
    | { kind: "circle"; radius: number }
    | { kind: "rectangle"; width: number; height: number }
    | { kind: "triangle"; a: number; b: number; c: number };
 
// never を使った網羅性チェック
function assertNever(x: never): never {
    throw new Error(`Unexpected value: ${x}`);
}
 
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": {
            const s = (shape.a + shape.b + shape.c) / 2;
            return Math.sqrt(s * (s-shape.a) * (s-shape.b) * (s-shape.c));
        }
        default:
            return assertNever(shape); // 新しい kind を追加したらコンパイルエラー
    }
}
 
// 型ガード関数
function isCircle(shape: Shape): shape is Extract<Shape, { kind: "circle" }> {
    return shape.kind === "circle";
}
 
function isRectangle(shape: Shape): shape is Extract<Shape, { kind: "rectangle" }> {
    return shape.kind === "rectangle";
}
 
// カスタム型ガード
function hasLength(value: unknown): value is { length: number } {
    return typeof value === "object" && value !== null && "length" in value;
}
 
// in 演算子による型の絞り込み
type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Pet = Fish | Bird;
 
function move(pet: Pet): void {
    if ("swim" in pet) {
        pet.swim(); // Fish として扱われる
    } else {
        pet.fly();  // Bird として扱われる
    }
}

Haskell のパターンマッチ

-- Haskell: 高度なパターンマッチ
 
-- ガード条件
bmi :: Double -> String
bmi x
    | x < 18.5  = "やせ型"
    | x < 25.0  = "普通体重"
    | x < 30.0  = "肥満(1度)"
    | otherwise  = "肥満(2度以上)"
 
-- as パターン(全体と部分を同時に束縛)
firstLetter :: String -> String
firstLetter ""         = "空文字列"
firstLetter all@(x:_)  = "'" ++ all ++ "' の先頭は '" ++ [x] ++ "'"
 
-- ビューパターン(GHC拡張)
-- {-# LANGUAGE ViewPatterns #-}
-- isEven :: Int -> Bool
-- process (isEven -> True) = "偶数"
-- process _                = "奇数"
 
-- case 式
describeList :: [a] -> String
describeList xs = "The list is " ++ case xs of
    []  -> "empty."
    [_] -> "a singleton."
    _   -> "a longer list of " ++ show (length xs) ++ " elements."
 
-- where 句との組み合わせ
calcTriangleType :: Double -> Double -> Double -> String
calcTriangleType a b c
    | a == b && b == c = "正三角形"
    | a == b || b == c || a == c = "二等辺三角形"
    | isRight = "直角三角形"
    | otherwise = "不等辺三角形"
  where
    sides = sort [a, b, c]
    isRight = abs (sides!!0^2 + sides!!1^2 - sides!!2^2) < 1e-10

4. 「不正な状態を表現不可能にする」

悪い設計(不正な状態が可能)

// 不正な状態が表現可能
interface Connection {
    status: "disconnected" | "connecting" | "connected" | "error";
    socket?: WebSocket;       // connected の時だけ存在
    error?: Error;            // error の時だけ存在
    retryCount?: number;      // error の時だけ意味がある
}
 
// 問題: status: "disconnected" なのに socket がある状態が作れてしまう
const invalid: Connection = {
    status: "disconnected",
    socket: new WebSocket("ws://..."),  // 不正だが型エラーにならない
};
 
// 別の問題: Optional フィールドの組み合わせ爆発
// socket の有無 × error の有無 × retryCount の有無 = 8 通り
// 有効な組み合わせは 4 通りだけ → 4 通りが不正な状態

良い設計(不正な状態が表現不可能)

// 不正な状態が型で表現できない
type Connection =
    | { status: "disconnected" }
    | { status: "connecting" }
    | { status: "connected"; socket: WebSocket }
    | { status: "error"; error: Error; retryCount: number };
 
// status: "disconnected" に socket を持たせることが型レベルで不可能
 
// 状態遷移関数も型安全に
function connect(conn: Extract<Connection, { status: "disconnected" }>): Extract<Connection, { status: "connecting" }> {
    return { status: "connecting" };
}
 
function onConnected(
    conn: Extract<Connection, { status: "connecting" }>,
    socket: WebSocket
): Extract<Connection, { status: "connected" }> {
    return { status: "connected", socket };
}
 
function onError(
    conn: Connection,
    error: Error
): Extract<Connection, { status: "error" }> {
    const retryCount = conn.status === "error" ? conn.retryCount + 1 : 0;
    return { status: "error", error, retryCount };
}
// Rust: enum で状態遷移を厳密にモデリング
enum ConnectionState {
    Disconnected,
    Connecting,
    Connected { socket: TcpStream },
    Error { error: io::Error, retry_count: u32 },
}
 
// Connected 状態でしか socket にアクセスできない
fn send_data(state: &ConnectionState, data: &[u8]) -> Result<(), String> {
    match state {
        ConnectionState::Connected { socket } => {
            // socket を安全に使用
            Ok(())
        }
        _ => Err("Not connected".to_string()),
    }
}
 
// 型状態パターンによるさらに厳密な表現
struct Disconnected;
struct Connecting;
struct Connected { socket: TcpStream }
struct ErrorState { error: io::Error, retry_count: u32 }
 
struct Connection<S> {
    state: S,
    config: ConnectionConfig,
}
 
impl Connection<Disconnected> {
    fn connect(self) -> Connection<Connecting> {
        Connection {
            state: Connecting,
            config: self.config,
        }
    }
}
 
impl Connection<Connecting> {
    fn on_connected(self, socket: TcpStream) -> Connection<Connected> {
        Connection {
            state: Connected { socket },
            config: self.config,
        }
    }
 
    fn on_error(self, error: io::Error) -> Connection<ErrorState> {
        Connection {
            state: ErrorState { error, retry_count: 0 },
            config: self.config,
        }
    }
}
 
impl Connection<Connected> {
    fn send(&mut self, data: &[u8]) -> io::Result<usize> {
        self.state.socket.write(data)
    }
 
    fn disconnect(self) -> Connection<Disconnected> {
        Connection {
            state: Disconnected,
            config: self.config,
        }
    }
}
// Connected 状態でのみ send が呼べる → コンパイル時に保証

実践例: API レスポンス

// ローディング状態を ADT で表現
type AsyncData<T, E = Error> =
    | { state: "idle" }
    | { state: "loading"; abortController?: AbortController }
    | { state: "success"; data: T; fetchedAt: Date }
    | { state: "error"; error: E; retryCount: number };
 
// ヘルパー関数
function idle<T>(): AsyncData<T> {
    return { state: "idle" };
}
 
function loading<T>(abortController?: AbortController): AsyncData<T> {
    return { state: "loading", abortController };
}
 
function success<T>(data: T): AsyncData<T> {
    return { state: "success", data, fetchedAt: new Date() };
}
 
function error<T>(err: Error, retryCount: number = 0): AsyncData<T> {
    return { state: "error", error: err, retryCount };
}
 
// React コンポーネントでの使用
function renderUser(user: AsyncData<User>) {
    switch (user.state) {
        case "idle":
            return <div>Press load</div>;
        case "loading":
            return <Spinner />;
        case "success":
            return <UserCard user={user.data} />;
        case "error":
            return <ErrorMessage error={user.error} retryCount={user.retryCount} />;
    }
}
 
// マップ関数
function mapAsyncData<T, U, E = Error>(
    data: AsyncData<T, E>,
    fn: (value: T) => U
): AsyncData<U, E> {
    if (data.state === "success") {
        return { ...data, data: fn(data.data) };
    }
    return data as AsyncData<U, E>;
}

実践例: フォームバリデーション

// フォームフィールドの状態
type FieldState<T> =
    | { status: "pristine" }
    | { status: "touched"; value: T }
    | { status: "valid"; value: T }
    | { status: "invalid"; value: T; errors: string[] };
 
// フォーム全体の状態
type FormState<T extends Record<string, unknown>> = {
    fields: { [K in keyof T]: FieldState<T[K]> };
    submitted: boolean;
};
 
// フォームがsubmit可能かどうかの判定
function canSubmit<T extends Record<string, unknown>>(form: FormState<T>): boolean {
    return Object.values(form.fields).every(
        (field) => (field as FieldState<unknown>).status === "valid"
    );
}
 
// バリデーション規則の ADT
type ValidationRule<T> =
    | { type: "required"; message: string }
    | { type: "minLength"; min: number; message: string }
    | { type: "maxLength"; max: number; message: string }
    | { type: "pattern"; regex: RegExp; message: string }
    | { type: "custom"; validate: (value: T) => boolean; message: string };
 
function validateField<T>(value: T, rules: ValidationRule<T>[]): string[] {
    const errors: string[] = [];
    for (const rule of rules) {
        switch (rule.type) {
            case "required":
                if (value === null || value === undefined || value === "") {
                    errors.push(rule.message);
                }
                break;
            case "minLength":
                if (typeof value === "string" && value.length < rule.min) {
                    errors.push(rule.message);
                }
                break;
            case "maxLength":
                if (typeof value === "string" && value.length > rule.max) {
                    errors.push(rule.message);
                }
                break;
            case "pattern":
                if (typeof value === "string" && !rule.regex.test(value)) {
                    errors.push(rule.message);
                }
                break;
            case "custom":
                if (!rule.validate(value)) {
                    errors.push(rule.message);
                }
                break;
        }
    }
    return errors;
}

実践例: 権限モデル

// Rust: ADT による権限モデル
enum Permission {
    Read,
    Write,
    Delete,
    Admin,
}
 
enum Role {
    Anonymous,
    User { id: UserId, permissions: Vec<Permission> },
    Moderator { id: UserId, managed_areas: Vec<String> },
    Admin { id: UserId },
}
 
enum AccessResult {
    Allowed,
    Denied { reason: String },
    RequiresAuthentication,
    RequiresElevation { required_role: String },
}
 
fn check_access(role: &Role, resource: &str, action: &Permission) -> AccessResult {
    match (role, action) {
        (Role::Admin { .. }, _) => AccessResult::Allowed,
        (Role::Anonymous, Permission::Read) => AccessResult::Allowed,
        (Role::Anonymous, _) => AccessResult::RequiresAuthentication,
        (Role::User { permissions, .. }, action) => {
            if permissions.iter().any(|p| std::mem::discriminant(p) == std::mem::discriminant(action)) {
                AccessResult::Allowed
            } else {
                AccessResult::Denied {
                    reason: format!("Insufficient permissions for {:?}", action),
                }
            }
        }
        (Role::Moderator { managed_areas, .. }, _) => {
            if managed_areas.iter().any(|a| resource.starts_with(a)) {
                AccessResult::Allowed
            } else {
                AccessResult::Denied {
                    reason: "Outside managed area".to_string(),
                }
            }
        }
    }
}

5. Null の問題と Option/Maybe

「10億ドルの間違い」(Tony Hoare, null の発明者)

問題: null は型システムの穴
  Java:    String name = null;  // NullPointerException の温床
  JS:      let x = null;       // TypeError: Cannot read property ...
  C:       char *p = NULL;     // セグメンテーションフォルト

解決: Option型 / Maybe型
  Rust:    Option<String>  → Some("Gaku") | None
  Haskell: Maybe String    → Just "Gaku" | Nothing
  Swift:   String?         → "Gaku" | nil
  Scala:   Option[String]  → Some("Gaku") | None
  Kotlin:  String?         → "Gaku" | null(コンパイラが追跡)

  値がないことを「型で明示」し、
  パターンマッチで安全に処理を強制する

Rust の Option 詳細

// Rust: null がない。Option で明示
fn find_user(id: u32) -> Option<User> {
    if id == 1 {
        Some(User { name: "Gaku".into(), age: 30 })
    } else {
        None
    }
}
 
// 使う側は None の可能性を必ず処理する
match find_user(1) {
    Some(user) => println!("Found: {}", user.name),
    None => println!("Not found"),
}
 
// メソッドチェーン
let name = find_user(1)
    .map(|u| u.name)
    .unwrap_or("Unknown".into());
 
// ? 演算子(エラー伝播)
fn get_user_name(id: u32) -> Option<String> {
    let user = find_user(id)?;  // None なら早期リターン
    Some(user.name)
}
 
// Option のコンビネータ群
fn process_user(id: u32) {
    let user = find_user(id);
 
    // map: Some の中身を変換
    let name: Option<String> = user.as_ref().map(|u| u.name.clone());
 
    // and_then (flatMap): ネストした Option を平坦化
    let email: Option<String> = find_user(id)
        .and_then(|u| find_email(u.id));
 
    // or_else: None の場合の代替
    let backup_user: Option<User> = find_user(id)
        .or_else(|| find_user_by_name("default"));
 
    // filter: 条件を満たさなければ None
    let adult: Option<User> = find_user(id)
        .filter(|u| u.age >= 18);
 
    // zip: 2つの Option を組み合わせ
    let pair: Option<(User, Config)> = find_user(id)
        .zip(load_config());
 
    // unwrap_or_else: None の場合にクロージャで値を生成
    let user_or_default: User = find_user(id)
        .unwrap_or_else(|| User::default());
 
    // is_some, is_none: 存在チェック
    if find_user(id).is_some() {
        println!("User exists");
    }
}
 
// Option<Option<T>> のフラット化
fn find_setting(key: &str) -> Option<Option<String>> {
    // 設定キーが存在しない → None
    // 設定キーが存在するが値が空 → Some(None)
    // 設定キーが存在し値がある → Some(Some(value))
    todo!()
}
 
let flat: Option<String> = find_setting("key").flatten();

Result との組み合わせ

// Option と Result の変換
fn find_user_or_error(id: u32) -> Result<User, String> {
    find_user(id).ok_or(format!("User {} not found", id))
}
 
fn try_find_user(id: u32) -> Option<User> {
    query_database(id).ok() // Result<User, DbError> → Option<User>
}
 
// 複数の Option/Result を組み合わせるパターン
fn create_full_profile(user_id: u32) -> Result<FullProfile, String> {
    let user = find_user(user_id)
        .ok_or("User not found")?;
    let address = find_address(user_id)
        .ok_or("Address not found")?;
    let preferences = find_preferences(user_id)
        .unwrap_or_default();
 
    Ok(FullProfile { user, address, preferences })
}
 
// collect で Vec<Result<T, E>> → Result<Vec<T>, E>
fn parse_all_numbers(inputs: &[&str]) -> Result<Vec<i32>, std::num::ParseIntError> {
    inputs.iter()
        .map(|s| s.parse::<i32>())
        .collect()
}

各言語の Null 安全性

// Kotlin: Null 安全性
fun findUser(id: Int): User? {
    return if (id == 1) User("Gaku", 30) else null
}
 
// 安全呼び出し演算子 (?.)
val name: String? = findUser(1)?.name
 
// エルビス演算子 (?:)
val nameOrDefault: String = findUser(1)?.name ?: "Unknown"
 
// 非 null アサーション (!!) — 危険
val forceUnwrap: String = findUser(1)!!.name // NullPointerException の可能性
 
// let でスコープを限定
findUser(1)?.let { user ->
    println("Found: ${user.name}")
    println("Age: ${user.age}")
}
 
// スマートキャスト
fun processUser(user: User?) {
    if (user != null) {
        // ここでは user は User(非null)として扱われる
        println(user.name)
    }
}
// Swift: Optional
func findUser(id: Int) -> User? {
    return id == 1 ? User(name: "Gaku", age: 30) : nil
}
 
// Optional バインディング
if let user = findUser(id: 1) {
    print("Found: \(user.name)")
}
 
// guard let(早期リターン)
func processUser(id: Int) -> String {
    guard let user = findUser(id: id) else {
        return "Not found"
    }
    return "User: \(user.name)"
}
 
// Optional チェイニング
let name = findUser(id: 1)?.name
 
// nil 合体演算子
let nameOrDefault = findUser(id: 1)?.name ?? "Unknown"
 
// map / flatMap
let uppercaseName = findUser(id: 1).map { $0.name.uppercased() }

6. 再帰的データ型

// Rust: リンクリスト
enum List<T> {
    Cons(T, Box<List<T>>),
    Nil,
}
 
impl<T: std::fmt::Display> List<T> {
    fn new() -> Self {
        List::Nil
    }
 
    fn prepend(self, value: T) -> Self {
        List::Cons(value, Box::new(self))
    }
 
    fn len(&self) -> usize {
        match self {
            List::Nil => 0,
            List::Cons(_, tail) => 1 + tail.len(),
        }
    }
 
    fn to_string(&self) -> String {
        match self {
            List::Nil => "Nil".to_string(),
            List::Cons(head, tail) => format!("{} -> {}", head, tail.to_string()),
        }
    }
}
 
let list = List::new()
    .prepend(3)
    .prepend(2)
    .prepend(1);
// 1 -> 2 -> 3 -> Nil
 
// 式の木(インタープリタの核心)
enum Expr {
    Num(f64),
    Var(String),
    Add(Box<Expr>, Box<Expr>),
    Mul(Box<Expr>, Box<Expr>),
    Let { name: String, value: Box<Expr>, body: Box<Expr> },
    If { cond: Box<Expr>, then_: Box<Expr>, else_: Box<Expr> },
    Lambda { param: String, body: Box<Expr> },
    Apply(Box<Expr>, Box<Expr>),
}
 
fn eval(expr: &Expr, env: &HashMap<String, f64>) -> Result<f64, String> {
    match expr {
        Expr::Num(n) => Ok(*n),
        Expr::Var(name) => env.get(name).copied()
            .ok_or_else(|| format!("Undefined variable: {}", name)),
        Expr::Add(left, right) => {
            Ok(eval(left, env)? + eval(right, env)?)
        }
        Expr::Mul(left, right) => {
            Ok(eval(left, env)? * eval(right, env)?)
        }
        Expr::Let { name, value, body } => {
            let val = eval(value, env)?;
            let mut new_env = env.clone();
            new_env.insert(name.clone(), val);
            eval(body, &new_env)
        }
        Expr::If { cond, then_, else_ } => {
            let c = eval(cond, env)?;
            if c != 0.0 { eval(then_, env) } else { eval(else_, env) }
        }
        _ => Err("Not implemented".to_string()),
    }
}

7. ジェネリック ADT

-- Haskell: ファンクタとしてのジェネリック ADT
data Tree a = Leaf | Node (Tree a) a (Tree a)
 
-- Functor インスタンス
instance Functor Tree where
    fmap _ Leaf         = Leaf
    fmap f (Node l x r) = Node (fmap f l) (f x) (fmap f r)
 
-- 使用例: 木の全要素を2倍にする
doubleTree :: Tree Int -> Tree Int
doubleTree = fmap (* 2)
 
-- Foldable インスタンス
instance Foldable Tree where
    foldMap _ Leaf         = mempty
    foldMap f (Node l x r) = foldMap f l <> f x <> foldMap f r
 
-- 木の要素の合計
sumTree :: Tree Int -> Int
sumTree = sum  -- Foldable のおかげで sum がそのまま使える
 
-- Free モナド(ADT の究極形)
data Free f a = Pure a | Free (f (Free f a))
 
-- Free モナドで DSL を構築
data ConsoleF next
    = ReadLine (String -> next)
    | PrintLine String next
 
type Console = Free ConsoleF
 
readLine :: Console String
readLine = Free (ReadLine Pure)
 
printLine :: String -> Console ()
printLine msg = Free (PrintLine msg (Pure ()))
 
-- DSL の使用例
greetProgram :: Console ()
greetProgram = do
    printLine "What is your name?"
    name <- readLine
    printLine ("Hello, " ++ name ++ "!")
// TypeScript: ジェネリック ADT
type Tree<T> =
    | { type: "leaf" }
    | { type: "node"; left: Tree<T>; value: T; right: Tree<T> };
 
function mapTree<T, U>(tree: Tree<T>, fn: (value: T) => U): Tree<U> {
    switch (tree.type) {
        case "leaf":
            return { type: "leaf" };
        case "node":
            return {
                type: "node",
                left: mapTree(tree.left, fn),
                value: fn(tree.value),
                right: mapTree(tree.right, fn),
            };
    }
}
 
function foldTree<T, U>(tree: Tree<T>, leaf: U, node: (left: U, value: T, right: U) => U): U {
    switch (tree.type) {
        case "leaf":
            return leaf;
        case "node":
            return node(
                foldTree(tree.left, leaf, node),
                tree.value,
                foldTree(tree.right, leaf, node)
            );
    }
}
 
// 使用例
const numTree: Tree<number> = {
    type: "node",
    left: { type: "node", left: { type: "leaf" }, value: 1, right: { type: "leaf" } },
    value: 2,
    right: { type: "node", left: { type: "leaf" }, value: 3, right: { type: "leaf" } },
};
 
const doubled = mapTree(numTree, x => x * 2);
const sum = foldTree(numTree, 0, (l, v, r) => l + v + r); // 6

実践演習

演習1: [基礎] -- 信号機をADTでモデリング

交通信号機の状態(赤・黄・青)を Rust の enum または TypeScript の判別ユニオンで実装する。各状態に持続時間を持たせ、次の状態への遷移関数を実装する。

演習2: [基礎] -- JSON パーサーの型定義

JSON の値(null, bool, number, string, array, object)を ADT で定義し、以下の関数を実装する:

  • stringify: JsonValue → String
  • get: JsonValue → path → Option
  • merge: JsonValue → JsonValue → JsonValue

演習3: [応用] -- 状態機械の実装

HTTP リクエストの状態遷移(Idle → Sending → Success/Error → Idle)を ADT で実装し、不正な遷移を型で防止する。リトライロジックも含める。

演習4: [応用] -- 式の評価器

四則演算・変数・let 束縛をサポートする小さな式言語のインタープリタを ADT で実装する。

演習5: [発展] -- 型安全なステートマシンライブラリ

Rust の型状態パターンを使って、任意の状態遷移図を型レベルで表現できる汎用的なステートマシンライブラリを実装する。


トラブルシューティング

よくあるエラーと解決策

エラー 原因 解決策
初期化エラー 設定ファイルの不備 設定ファイルのパスと形式を確認
タイムアウト ネットワーク遅延/リソース不足 タイムアウト値の調整、リトライ処理の追加
メモリ不足 データ量の増大 バッチ処理の導入、ページネーションの実装
権限エラー アクセス権限の不足 実行ユーザーの権限確認、設定の見直し
データ不整合 並行処理の競合 ロック機構の導入、トランザクション管理

デバッグの手順

  1. エラーメッセージの確認: スタックトレースを読み、発生箇所を特定する
  2. 再現手順の確立: 最小限のコードでエラーを再現する
  3. 仮説の立案: 考えられる原因をリストアップする
  4. 段階的な検証: ログ出力やデバッガを使って仮説を検証する
  5. 修正と回帰テスト: 修正後、関連する箇所のテストも実行する
# デバッグ用ユーティリティ
import logging
import traceback
from functools import wraps
 
# ロガーの設定
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s [%(levelname)s] %(name)s: %(message)s'
)
logger = logging.getLogger(__name__)
 
def debug_decorator(func):
    """関数の入出力をログ出力するデコレータ"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        logger.debug(f"呼び出し: {func.__name__}(args={args}, kwargs={kwargs})")
        try:
            result = func(*args, **kwargs)
            logger.debug(f"戻り値: {func.__name__} -> {result}")
            return result
        except Exception as e:
            logger.error(f"例外発生: {func.__name__}: {e}")
            logger.error(traceback.format_exc())
            raise
    return wrapper
 
@debug_decorator
def process_data(items):
    """データ処理(デバッグ対象)"""
    if not items:
        raise ValueError("空のデータ")
    return [item * 2 for item in items]

パフォーマンス問題の診断

パフォーマンス問題が発生した場合の診断手順:

  1. ボトルネックの特定: プロファイリングツールで計測
  2. メモリ使用量の確認: メモリリークの有無をチェック
  3. I/O待ちの確認: ディスクやネットワークI/Oの状況を確認
  4. 同時接続数の確認: コネクションプールの状態を確認
問題の種類 診断ツール 対策
CPU負荷 cProfile, py-spy アルゴリズム改善、並列化
メモリリーク tracemalloc, objgraph 参照の適切な解放
I/Oボトルネック strace, iostat 非同期I/O、キャッシュ
DB遅延 EXPLAIN, slow query log インデックス、クエリ最適化

設計判断ガイド

選択基準マトリクス

技術選択を行う際の判断基準を以下にまとめます。

判断基準 重視する場合 妥協できる場合
パフォーマンス リアルタイム処理、大規模データ 管理画面、バッチ処理
保守性 長期運用、チーム開発 プロトタイプ、短期プロジェクト
スケーラビリティ 成長が見込まれるサービス 社内ツール、固定ユーザー
セキュリティ 個人情報、金融データ 公開データ、社内利用
開発速度 MVP、市場投入スピード 品質重視、ミッションクリティカル

アーキテクチャパターンの選択

アーキテクチャ選択フロー
① チーム規模は?
├─ 小規模(1-5人)→ モノリス
└─ 大規模(10人+)→ ②へ
② デプロイ頻度は?
├─ 週1回以下 → モノリス + モジュール分割
└─ 毎日/複数回 → ③へ
③ チーム間の独立性は?
├─ 高い → マイクロサービス
└─ 中程度 → モジュラーモノリス

トレードオフの分析

技術的な判断には必ずトレードオフが伴います。以下の観点で分析を行いましょう:

1. 短期 vs 長期のコスト

  • 短期的に速い方法が長期的には技術的負債になることがある
  • 逆に、過剰な設計は短期的なコストが高く、プロジェクトの遅延を招く

2. 一貫性 vs 柔軟性

  • 統一された技術スタックは学習コストが低い
  • 多様な技術の採用は適材適所が可能だが、運用コストが増加

3. 抽象化のレベル

  • 高い抽象化は再利用性が高いが、デバッグが困難になる場合がある
  • 低い抽象化は直感的だが、コードの重複が発生しやすい
# 設計判断の記録テンプレート
class ArchitectureDecisionRecord:
    """ADR (Architecture Decision Record) の作成"""
 
    def __init__(self, title: str):
        self.title = title
        self.context = ""
        self.decision = ""
        self.consequences = []
        self.alternatives = []
 
    def set_context(self, context: str):
        """背景と課題の記述"""
        self.context = context
        return self
 
    def set_decision(self, decision: str):
        """決定内容の記述"""
        self.decision = decision
        return self
 
    def add_consequence(self, consequence: str, positive: bool = True):
        """結果の追加"""
        self.consequences.append({
            'description': consequence,
            'type': 'positive' if positive else 'negative'
        })
        return self
 
    def add_alternative(self, name: str, reason_rejected: str):
        """却下した代替案の追加"""
        self.alternatives.append({
            'name': name,
            'reason_rejected': reason_rejected
        })
        return self
 
    def to_markdown(self) -> str:
        """Markdown形式で出力"""
        md = f"# ADR: {self.title}\n\n"
        md += f"## 背景\n{self.context}\n\n"
        md += f"## 決定\n{self.decision}\n\n"
        md += "## 結果\n"
        for c in self.consequences:
            icon = "✅" if c['type'] == 'positive' else "⚠️"
            md += f"- {icon} {c['description']}\n"
        md += "\n## 却下した代替案\n"
        for a in self.alternatives:
            md += f"- **{a['name']}**: {a['reason_rejected']}\n"
        return md

FAQ

Q1: このトピックを学ぶ上で最も重要なポイントは何ですか?

実践的な経験を積むことが最も重要です。理論だけでなく、実際にコードを書いて動作を確認することで理解が深まります。

Q2: 初心者がよく陥る間違いは何ですか?

基礎を飛ばして応用に進むことです。このガイドで説明している基本概念をしっかり理解してから、次のステップに進むことをお勧めします。

Q3: 実務ではどのように活用されていますか?

このトピックの知識は、日常的な開発業務で頻繁に活用されます。特にコードレビューやアーキテクチャ設計の際に重要になります。


まとめ

概念 説明
直積型 A かつ B(全フィールド) struct, interface
直和型 A または B(1つだけ) enum, union type
パターンマッチ 網羅的な分岐処理 match, switch
Option/Maybe null の安全な代替 Some/None
状態モデリング 不正な状態を型で防止 判別ユニオン
ニュータイプ 型安全なラッパー newtype, ブランド型
再帰的 ADT 自己参照するデータ構造 List, Tree, Expr
ジェネリック ADT 型パラメータ付き ADT Tree, Result<T,E>
言語 直和型サポート パターンマッチ 網羅性チェック
Rust enum(最高レベル) match, if let コンパイル時
Haskell data(最高レベル) case, 関数定義 コンパイル時(警告)
TypeScript 判別ユニオン switch + 型ガード never チェック
Kotlin sealed class when コンパイル時
Swift enum + associated values switch コンパイル時
Scala sealed trait match コンパイル時(警告)
Go インターフェース + 型スイッチ switch v.(type) なし
Java sealed interface(17+) switch(21+ パターン) コンパイル時
Python dataclass + Union match(3.10+) なし

次に読むべきガイド


参考文献

  1. Pierce, B. "Types and Programming Languages." MIT Press, 2002.
  2. Wlaschin, S. "Domain Modeling Made Functional." Pragmatic Bookshelf, 2018.
  3. Swierstra, W. "Data Types a la Carte." JFP, 2008.
  4. Yorgey, B. "The Typeclassopedia." The Monad.Reader, 2009.
  5. Klabnik, S. & Nichols, C. "The Rust Programming Language." Ch.6 (Enums and Pattern Matching), 2023.
  6. Hoare, C.A.R. "Null References: The Billion Dollar Mistake." QCon, 2009.
  7. Bloch, J. "Effective Java." 3rd Ed, Item 55 (Return optionals judiciously), 2018.
  8. Rust RFC 2005: "Match Ergonomics." 2017.