Remove associated type inference
01 何が問題だったのか
Swift では、associated type を持つプロトコルに適合する際、その associated type を typealias で明示的に指定しなくても、要件の実装から型チェッカーが自動的に推論してくれます。たとえば次のコードでは、StringBag.Element が String であることを、object(at:) の戻り値から推論しています。
protocol SimpleCollection {
associatedtype Element
func object(at index: Int) -> Element?
}
class StringBag: SimpleCollection {
func object(at index: Int) -> String? {
// ...
}
}
この associated type の推論(associated type witness inference)は、Swift の中で唯一「グローバル型推論」を必要とする機能です。ローカルに閉じた推論では決まらず、型全体をまたいで一貫した推論を行う必要があるため、型チェッカーの実装を大きく複雑にしており、次のような問題を引き起こしてきました。
- バグ・クラッシュ・直感に反する挙動が多く発生している
- 型チェッカーの性能と正確性の改善を阻む大きな要因になっている
- 推論結果が十分に予測可能ではなく、プロトコル拡張内の
typealiasで associated type 要件を満たせない、デフォルト付き associated type の扱いが不規則、といった副作用的な制約も生んでいる
この提案は、associated type の推論そのものを Swift から取り除き、適合する型が自分の associated type を明示的に決めなければならないようにすることで、型チェッカーからグローバル型推論を完全に排除することを目的としていました。
提案の帰結
Swift Evolution のレビューを経てこの提案は Rejected(却下)となり、associated type の推論は Swift に残されています。レビューでは、Collection のように推論可能な associated type を多く持つプロトコルに適合する際に記述量が大幅に増え、プロトコルの使いやすさが大きく損なわれることが懸念点として挙げられました。
02 どのように解決されるのか
提案は却下されたため、現在の Swift でも associated type は従来どおり推論されます。以下では、提案で示されていた「もし推論が削除されたらどう書くことになるか」の設計を整理します。
typealias による明示的な指定
適合する型の中(または後からの extension)で、typealias によって associated type を束縛します。
class StringBag: SimpleCollection {
typealias Element = String
func object(at index: Int) -> String? { /* ... */ }
}
ネストした型による指定
associated type と同じ名前のネスト型を定義することで、それをそのまま associated type として使う形も許容されます。
class FooBag: SimpleCollection {
struct Element { /* ... */ }
func object(at index: Int) -> Element? { /* ... */ }
}
デフォルト型の利用
プロトコル側で associatedtype A = Int のようにデフォルト型が指定されていれば、適合側では何も書かずにそのデフォルトを採用できます。推論の仕組みを外したことにより、プロトコル拡張内の typealias で associated type のデフォルト値を与えたり、条件付きのデフォルトを与えたりすることも自然に表現できるようになる、とされていました。
protocol P {
associatedtype A
associatedtype B
}
extension P where A: Fooable {
typealias B = Int
}
class C1: P {
// C1.A is not Fooable
struct A { /* ... */ }
// 'C1.B' は明示的な指定が必要
typealias B = String
}
class C2: P {
// C2.A is Fooable
struct A: Fooable { /* ... */ }
// 'C2.B' は暗黙的に Int
}
セマンティクスの変化に注意
推論を削除すると、プロトコル拡張のデフォルト実装と適合側の実装の選ばれ方が変わる、という意味論上の変化もあります。次の例はその代表例です。
protocol P {
associatedtype A = Int
func doSomething() -> A
}
extension P {
func doSomething() -> Int {
return 50
}
}
class C: P {
func doSomething() -> String {
return "hello"
}
}
func myMethod<T: P>(_ x: T) -> T.A {
return x.doSomething()
}
現在の Swift では C.A は String と推論され、String を返す doSomething() が要件を満たす実装として選ばれます。一方、推論を削除した場合は typealias が書かれていないため C.A はデフォルトの Int となり、プロトコル拡張側の Int を返す doSomething() が要件を満たす実装として採用されます。ソースコードは同じでも、挙動が静かに変わってしまう点が問題として指摘されていました。
提案ではこの種の「意図せず要件を満たしてしまう/満たせない」問題に対し、要件を満たすように見えて実は満たしていないメンバに警告を出すなど、型チェッカーのヒューリスティクスで緩和する方向が示されていました。
却下の背景
前述のとおりこの提案は却下されており、associated type の推論はそのまま Swift に残っています。型チェッカーの複雑さというコンパイラ側の課題は別途、実装面の改善によって段階的に取り組まれていく位置づけとなりました。