Revision
Rev. | Date | Note |
---|---|---|
0.1.0 |
2025-02 | 社内技術トーク向けに作成 |
0.1.1 |
2025-02 | 参考サイトを追加 |
動機
代数的データ型の活用がこれからのプログラミングに必要となるため。
これからのプログラミングは、関数型プログラミングのエッセンスを取り入れ、なるべくコンパイル時にバグを検知できるようにする必要がある。そのために必要な要素が、代数的データ型を意識した型定義である。オブジェクト指向のクラス定義から一歩足を踏み出し、関数型プログラミング由来の代数的データ型定義を活用して、安全でバグの少ないプログラミングをする方法を身につけよう。
代数的データ型とは何か
代数的データ型(Algebraic Data Type, ADT)とは、代数学が扱う集合概念をデータ型に応用したもの。集合を分析する際の積集合、和集合でデータ型を構成することにより、コンパイル時の型チェックを活用するプログラミングテクニック。
積集合型
- タプル、構造体、クラス
- 集合要素の組み合わせで表現する構造(非排他的)
- 構築時の要素の初期化チェック、利用時の要素の存在チェック
和集合型
- 列挙型
- 集合要素のいずれかで表現する構造(排他的)
- 要素の網羅性チェック
代数的データ型の積集合、和集合を構成することは従来の型システムを持つプログラム言語でも可能だが、和集合型の表現力強化(異なる型の包含)や網羅性をチェックするためには言語システムのサポートが必要となる。
Java の和集合型への対応強化1
-
Java 12 以降の
switch
式を使うことで、すべてのenum
値を網羅していない場合にコンパイル時エラーになる -
Java 15 以降の
sealed
クラスを使うことで、和集合型をより強力に表現できる(Rust の Option型、Result型のような型を作成できる) -
Java 17 以降の
switch
式を使うことで、すべてのsealed
クラスを網羅していない場合にコンパイル時エラーになる
(正式な仕様としては Java 21 から)
手法 | 網羅性チェック | コンパイル時エラー | 実行時エラー | 対応バージョン |
---|---|---|---|---|
switch 文 (enum) |
手動 | 警告レベル次第 | あり | Java 5+ |
switch 式 |
自動 | ✅ | なし | Java 12+ |
sealed クラス |
自動 | ✅ | なし | Java 15+ |
EnumSet ユーティリティ |
手動 | なし | あり | Java 5+ |
和集合型の重要性
和集合型は「集合要素のどれか一つを必ず選ぶ」という構造を表現する。この特性により、パターンマッチングですべての選択肢を網羅しているかをコンパイル時にチェックできる。
✅ 網羅性チェックのメリット
- バグの早期検知
→ 新しい選択肢が追加された場合に、網羅性が不足しているとコンパイル時エラーにできる。 - コードの可読性向上
→ 各ケースが明確になり、未処理のケースを防ぐ。
和集合型による状態管理
網羅性のチェックができるということは状態管理との親和性が高いということである。意図しない状態の混入と状態を追加した場合の考慮漏れを防ぐことが可能になる。
❌ 従来の状態管理の課題
従来の状態管理では、状態をString
やint
などで表現し、条件分岐(if-else
やswitch
)で状態を判定することが多かった。しかし、これでは状態の網羅性が保証されず、未処理の状態が発生するリスクがあった。
✅ 和集合型を状態管理に使うメリット
状態が排他的であり、同時に複数の状態に遷移しない場合、和集合型を使うことで次のようなメリットが得られる。
- 状態の網羅性をコンパイル時にチェック可能
- 各状態が排他的であることの保証
- 状態考慮もれのバグを未然に防止
- 可読性と保守性の向上
和集合型による不要な状態の排除
和集合型の「集合要素のどれか一つを必ず選ぶ」という特性を利用して適切な型設計を行えば、不要な状態を排除することもできる。
積集合型の問題
例えば、以下のような積集合でUser
型を構成するクラスがあった場合、
class User {
MemberId memberId;
GuestId guestId;
}
仕様上MemberId
、GuestId
のいずれかのみを想定していたとしても、要素の組み合わせのため可能な状態の数は 2 x 2 = 4 となってしまう。
User { memberId, guestId } // 仕様外の不要な状態
User { memberId, null }
User { null, guestId }
User { null, null } // 仕様外の不要な状態
テストコードやバリデーションコードで仕様外の状態を防ぐことはできるが、そのコードも人力で作成されるため抜け漏れを完全に防ぐことはできない。くわえてAdminId
などを追加してUser
の対象範囲が広がれば、さらに不要な状態が増加することになってしまう。
積集合による型の構成は要素の組み合わせとなるため、不用意に1つの型にまとめようとすると不要な状態まで内包してしまうことが問題となる。
和集合型による解決
和集合でUser
型を構成すると以下のようになる。
// 和集合型の定義
sealed interface User permits MemberUser, GuestUser {}
// 集合要素型の実装
class MemberUser implements User {
MemberId memberId;
MemberUser(MemberId memberId) {
if (memberId == null) {
throw new IllegalArgumentException("MemberId cannot be null");
}
this.memberId = memberId;
}
}
// 集合要素型の実装
class GuestUser implements User {
GuestId guestId;
GuestUser(GuestId guestId) {
if (guestId == null) {
throw new IllegalArgumentException("GuestId cannot be null");
}
this.guestId = guestId;
}
}
コンストラクタでnull
状態を排除しつつ、sealed
によって実装を制限することでUser
型はMemberUser
、GuestUser
のいずれかとなり、排他的に状態を表現することが可能となる。
(実行時例外で排除するしかないのがnull
を許容した言語のツラいところ😣)
まとめ
型はデータの集合であり、データは状態でもある。これまでの型の使い方は状態の組み合わせが中心であり、状態の組み合わせはロジックに複雑性をもたらしバグの温床となってきた。
これからのプログラミングは、型により取りえる状態を制限することで複雑性を低減し、さらに言語システムのチェックを活用することで安全でバグの少ないソフトウェアを作成していく必要がある。
【キーフレーズ】
- 和集合型で不要な状態の排除と自動チェック
ちなみに生成 AI では、このような一般的に集合知となっていないコードは生成することができない。集合知になるようにコードを作成していきましょう。
Appendix
参考サイト
-
Java の言語仕様の変更については ChatGPT による。言語仕様まとめはJavaバージョン履歴を参照 ↩