🎯

直和型(Sum Type)の説明

に公開

直和型とは

直和型は、複数の型のうち「どれか1つ」を表現できる型のことです。数学の集合論における「直和集合」から名付けられています。

「ちょくわ」と読むらしい?
仕事で今日はじめて聞いた。

昔の学習の参考になりそうなスライドもありました。こちらにリンクを貼っておきます。
https://speakerdeck.com/takasek/20190730-sekkeikaigi

簡単な例

数学的な表現

集合A = {1, 2} と 集合B = {3, 4} の直和は:
A ∪ B = {1, 2, 3, 4}

つまり、値は「AまたはB」のどちらかに属することになります。

プログラミングでの表現

こちらがサンプル

// 直和型の実装例

// 1. シンプルなenum(従来の列挙型)
enum UserRole {
  admin,
  user,
  guest
}

// 2. 直和型(sealed class)による実装
sealed class UserAction {
  const UserAction();
}

final class Login extends UserAction {
  final String username;
  final String password;
  const Login(this.username, this.password);
}

final class Logout extends UserAction {
  final DateTime logoutTime;
  const Logout(this.logoutTime);
}

final class UpdateProfile extends UserAction {
  final Map<String, dynamic> newData;
  const UpdateProfile(this.newData);
}

// アクションを処理する関数
String handleUserAction(UserAction action) {
  return switch (action) {
    Login(username: final user, password: final _) => 
        'ログイン: $user',
    Logout(logoutTime: final time) => 
        'ログアウト時刻: $time',
    UpdateProfile(newData: final data) => 
        'プロフィール更新: $data',
  };
}

void main() {
  // 1. enumの使用例
  final role = UserRole.admin;
  print('ユーザーロール: ${role.name}');
  
  // 2. 直和型の使用例
  final actions = [
    Login('user123', 'password123'),
    UpdateProfile({'name': '新しい名前', 'age': 25}),
    Logout(DateTime.now()),
  ];
  
  print('\n--- ユーザーアクション ---');
  for (final action in actions) {
    print(handleUserAction(action));
  }
  
  // パターンマッチングの網羅性チェック
  // 以下のようなswitch文を書くと、
  // 新しいアクションが追加された時にコンパイルエラーになる
  final someAction = actions.first;
  final result = switch (someAction) {
    Login _ => 'ログイン処理',
    Logout _ => 'ログアウト処理',
    UpdateProfile _ => 'プロフィール更新処理',
  };
  print('\n結果: $result');
}

実行結果:

ユーザーロール: admin

--- ユーザーアクション ---
ログイン: user123
プロフィール更新: {name: 新しい名前, age: 25}
ログアウト時刻: 2025-01-08 22:34:12.333460

結果: ログイン処理

Exited.

基本的な例

sealed class Shape {
  const Shape();
}

final class Circle extends Shape {
  final double radius;
  const Circle(this.radius);
}

final class Rectangle extends Shape {
  final double width;
  final double height;
  const Rectangle(this.width, this.height);
}

この例では:

  • ShapeCircleRectangleのどちらか一方になります
  • 同時に両方になることはできません
  • 他の形状になることもできません(sealed classの制約)

なぜ直和型が便利か

  1. 型安全性

    • コンパイル時に全てのケースを処理していることを確認できる
    • 未処理のケースがあるとコンパイルエラーになる
  2. データモデリング

    sealed class ApiResponse {
      const ApiResponse();
    }
    
    final class Success extends ApiResponse {
      final dynamic data;
      const Success(this.data);
    }
    
    final class Error extends ApiResponse {
      final String message;
      const Error(this.message);
    }
    
    • APIレスポンスは「成功」か「エラー」のどちらか
    • 両方同時には存在しない
  3. パターンマッチング

    final response = switch (apiResponse) {
      Success(data: final d) => '成功: $d',
      Error(message: final m) => 'エラー: $m',
    };
    

他言語での直和型

  1. Swift

    enum Result<T> {
        case success(T)
        case failure(Error)
    }
    
  2. Kotlin

    sealed class Result<T> {
        data class Success<T>(val data: T): Result<T>()
        data class Error<T>(val error: Exception): Result<T>()
    }
    

直和型 vs 列挙型

列挙型(単純なenum)

enum Color {
  red,
  green,
  blue
}
  • 単純な値の列挙
  • 追加のデータを持てない(Dart 2.17以前)

直和型

sealed class Color {
  const Color();
}

final class RGB extends Color {
  final int r, g, b;
  const RGB(this.r, this.g, this.b);
}

final class HSL extends Color {
  final double h, s, l;
  const HSL(this.h, this.s, this.l);
}
  • 複雑なデータ構造を持てる
  • 型の安全性が高い
  • パターンマッチングと相性が良い

ユースケース

  1. 状態管理

    sealed class LoadingState {
      const LoadingState();
    }
    
    final class Initial extends LoadingState {}
    final class Loading extends LoadingState {}
    final class Loaded extends LoadingState {
      final dynamic data;
      const Loaded(this.data);
    }
    final class Error extends LoadingState {
      final String message;
      const Error(this.message);
    }
    
  2. バリデーション結果

    sealed class ValidationResult {
      const ValidationResult();
    }
    
    final class Valid extends ValidationResult {}
    final class Invalid extends ValidationResult {
      final List<String> errors;
      const Invalid(this.errors);
    }
    

まとめ

直和型は:

  • 相互に排他的な状態や型を表現
  • 型安全性を確保
  • コードの可読性を向上
  • バグの早期発見に貢献

Dart 3.0では、sealed classとパターンマッチングの組み合わせで、
効果的に直和型を実現できるようになりました。

Discussion