where clauses on contextually generic declarations
01 何が問題だったのか
ジェネリックな型の内側で宣言されたメンバ(インナー宣言)に where 句を付けたいことはよくあります。たとえば次のように、Box<Wrapped> の Wrapped が Sequence に適合しているときだけ使えるメソッドを書きたい、というケースです。
struct Box<Wrapped> {
// 書きたいのはこういうコード
func boxes() -> [Box<Wrapped.Element>] where Wrapped: Sequence { ... }
}
しかしこれまでの Swift では、メンバ自身が追加のジェネリックパラメータを持たない限り、外側のジェネリックパラメータだけを参照する where 句をメンバに付けることができず、次のようなエラーになっていました。
'where' clause cannot be attached
そのため、制約付きのメソッドを書くには、メンバごとに where が一致する専用の extension を切り出す必要がありました。
struct Foo<T> {}
extension Foo where T: Sequence, T.Element: Equatable {
func slowFoo() { ... }
}
extension Foo where T: Sequence, T.Element: Hashable {
func optimizedFoo() { ... }
}
extension Foo where T: Sequence, T.Element == Character {
func specialCaseFoo() { ... }
}
制約が少しでも違うメンバは、それぞれ別の extension に分けざるを得ません。これは意味的に関連する API をひとまとめにする妨げになり、制約を段階的に積み増していくような設計も書きにくく、ジェネリクスを多用する API の見通しを悪くしていました。
本来であれば、「制約を書いて意味のある場所には、どこにでも where が書けてほしい」と考えるのが自然です。次のように、共通する制約は extension に置き、個別の追加制約はメンバ側の where で絞り込む、というスタイルも選べるべきだというわけです。
extension Foo where T: Sequence, T.Element: Equatable {
func slowFoo() { ... }
// 同じ extension の中で、メンバごとに追加制約を書きたい
func optimizedFoo() where T.Element: Hashable { ... }
func specialCaseFoo() where T.Element == Character { ... }
}
02 どのように解決されるのか
ジェネリックな文脈に置かれたメンバ宣言について、外側のジェネリックパラメータだけを参照する where 句を書けるようにします。これにより、従来は専用の extension を切り出さないと書けなかった制約を、メンバ宣言に直接付けられます。
struct Box<Wrapped> {
// Wrapped: Sequence のときだけ呼べるメソッド
func boxes() -> [Box<Wrapped.Element>] where Wrapped: Sequence {
// ...
}
}
extension と組み合わせれば、共通する制約は extension 側にまとめ、メンバ固有の追加制約だけをメンバ側に書く、というスタイルも選べるようになります。
extension Foo where T: Sequence, T.Element: Equatable {
func slowFoo() { ... }
func optimizedFoo() where T.Element: Hashable { ... }
func specialCaseFoo() where T.Element == Character { ... }
}
オーバーライド
メンバに付いた where 句は、実質的に「その制約を満たすときだけこのメンバが見える」という可視性の制約として働きます。そのため、オーバーライドはオーバーライド元よりも少なくとも同じだけ見える必要があります。言い換えると、オーバーライド側の where は、オーバーライド元の where を満たす型すべてに対して成り立っていなければなりません。
class Base<T> {
func foo() where T == Int { ... }
}
class Derived<T>: Base<T> {
// OK: <T: Equatable> で呼べる場面は <T == Int> で呼べる場面の上位集合
override func foo() where T: Equatable { ... }
}
今回のスコープ外
この提案がカバーするのは、すでにジェネリックパラメータリストを書ける種類のメンバ宣言に限られます。プロパティや、プロトコル要件に対する未サポートの制約は対象外です。
protocol P {
// NG: プロトコル要件の Self に制約を追加することはできない
func foo() where Self: Equatable
}
class C {
// NG: クラスの通常メソッドで Self に制約を付けることもできない
func foo() where Self: Equatable
}
一方、extension のメンバとして書く場合は Self への制約も書けるようになります。
extension P {
func bar() where Self: Equatable { ... }
}