🎯
直和型(Sum Type)の説明
直和型とは
直和型は、複数の型のうち「どれか1つ」を表現できる型のことです。数学の集合論における「直和集合」から名付けられています。
「ちょくわ」と読むらしい?
仕事で今日はじめて聞いた。
昔の学習の参考になりそうなスライドもありました。こちらにリンクを貼っておきます。
簡単な例
数学的な表現
集合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);
}
この例では:
-
Shape
はCircle
かRectangle
のどちらか一方になります - 同時に両方になることはできません
- 他の形状になることもできません(sealed classの制約)
なぜ直和型が便利か
-
型安全性
- コンパイル時に全てのケースを処理していることを確認できる
- 未処理のケースがあるとコンパイルエラーになる
-
データモデリング
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レスポンスは「成功」か「エラー」のどちらか
- 両方同時には存在しない
-
パターンマッチング
final response = switch (apiResponse) { Success(data: final d) => '成功: $d', Error(message: final m) => 'エラー: $m', };
他言語での直和型
-
Swift
enum Result<T> { case success(T) case failure(Error) }
-
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);
}
- 複雑なデータ構造を持てる
- 型の安全性が高い
- パターンマッチングと相性が良い
ユースケース
-
状態管理
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); }
-
バリデーション結果
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