Expression Problem
Philip Wadlerが1998年に名付けた、プログラミング言語設計における根本的なトレードオフ。
データ型に対して、既存コードを修正せずに「新しいバリアント」と「新しい操作」の両方を追加できるか?
2つの拡張軸
| 新しいバリアント追加 | 新しい操作追加 | |
|---|---|---|
| 判別共用体 + パターンマッチ | 全パターンマッチ修正 | 関数を1つ追加するだけ |
| クラス + ポリモーフィズム | サブクラスを1つ追加するだけ | 全クラスにメソッド追加 |
具体例
判別共用体(関数型)
type Shape = Circle | Rectangle; // ← バリアント
function area(s: Shape): number { // ← 操作
switch (s.type) {
case "circle": return Math.PI * s.radius ** 2;
case "rectangle": return s.width * s.height;
}
}
- Triangle追加:
area、perimeter、その他全関数を修正 - perimeter追加:新しい関数を1つ書くだけ
ポリモーフィズム(OOP)
interface Shape {
area(): number; // ← 操作
}
class Circle implements Shape { area() { ... } }
class Rectangle implements Shape { area() { ... } }
- Triangle追加:
Triangleクラスを1つ書くだけ - perimeter追加:全クラスに
perimeter()メソッド追加
「修正が必要」は悪いことか?
ここで重要な問い:パターンマッチの修正は「悪い結合」なのか?
考え方1:修正箇所が散らばるのは問題
- バリアント追加のたびに複数ファイルを修正
- 凝集度が下がる
考え方2:それは自然な依存関係
- そのデータを扱う処理なら、データの種類が増えれば処理も増えるのは当然
- TypeScriptのexhaustive checkで修正漏れはコンパイルエラーになる
- 「変更すべき箇所」が明示されるのはむしろ良いこと
→ これは制御結合とは異なる。データの構造に依存しているだけで、内部実装の分岐ロジックを知っているわけではない。
本質:どちらの拡張が頻繁か
Expression Problemは「どちらが良い」ではなく「トレードオフの選択」。
- バリアント追加が頻繁 → OOP(ポリモーフィズム)
- 操作追加が頻繁 → FP(判別共用体)
多くのドメインでは両方が起きるため、完全な解決は難しい。
解決へのアプローチ
- Visitor Pattern:OOPで操作追加を容易にする(が複雑)
- Type Classes(Haskell):両方の拡張を可能にする
- Object Algebras:学術的な解決策
関連
- 判別共用体 - 関数型アプローチ
- ポリモーフィズム - OOPアプローチ
- 制御結合 - 関連するが異なる問題
- 主語の位置:OOPと関数型における関数適用スタイルの違い - 関数適用スタイルの考察