Swift Digest
SE-0267 | Swift Evolution

where clauses on contextually generic declarations

Proposal
SE-0267
Authors
Anthony Latsis
Review Manager
Joe Groff
Status
Implemented (Swift 5.3)

01 何が問題だったのか

ジェネリックな型の内側で宣言されたメンバ(インナー宣言)に where 句を付けたいことはよくあります。たとえば次のように、Box<Wrapped>WrappedSequence に適合しているときだけ使えるメソッドを書きたい、というケースです。

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 { ... }
}