Swift Digest
SE-0142 | Swift Evolution

Permit where clauses to constrain associated types

Proposal
SE-0142
Authors
David Hart, Jacob Bandes-Storch, Doug Gregor
Review Manager
Doug Gregor
Status
Implemented (Swift 4.0)

01 何が問題だったのか

プロトコルが持つ関連型(associated type)に対して、これまでは単純な継承の制約しか書けませんでした。一方、ジェネリック型パラメータに対しては where 句を使って「別の関連型と一致すること」「別のプロトコルに適合すること」など、より踏み込んだ制約を表現できます。関連型にも同等の表現力を持たせられないために、標準ライブラリを含む多くの設計が不自然なかたちを取らざるを得ない状況でした。

関連型の制約が継承指定だけで足りない

プロトコルの関連型宣言では、次のような「この関連型はこのプロトコルに適合している」という継承風の制約しか書けませんでした。

protocol Sequence {
    associatedtype Iterator: IteratorProtocol
    associatedtype SubSequence: Sequence
    // ...
}

ここで本来書きたいのは、「SubSequence もまた Sequence であり、しかもその要素型が自身の要素型と一致している」という条件です。従来の文法ではこの「関連型同士の一致」や「関連型がさらに別のプロトコルに適合する」といった関係を、関連型の宣言側でそのまま表現する手段がありませんでした。

制約を外側に逃がすしかなかった

結果として、こうした制約は関連型の宣言から切り離し、プロトコルを使うジェネリック関数側の where 句や、プロトコルを継承した別のプロトコル側に書き散らす必要がありました。関連型に本来備わっているべき不変条件がプロトコル定義の中に収まらず、利用箇所ごとに同じ制約を繰り返し書いたり、標準ライブラリの Collection のように「条件を表現するためだけにプロトコルを分割する」といった回避策が必要だったのです。

親プロトコルの関連型に制約を追加できない

プロトコルを継承して新しいプロトコルを作るときにも、親プロトコルの関連型に対して追加の制約を課したいことがあります。たとえば「Sequence の一種だが、その Iterator.ElementInt に限る」というような絞り込みです。従来は、プロトコル宣言の where 句から親プロトコルの関連型を参照する文法が用意されていなかったため、こうした絞り込みも自然には書けませんでした。

02 どのように解決されるのか

関連型の宣言に where 句を書けるようにし、加えてプロトコル宣言自体の where 句から親プロトコルの関連型を参照できるようにします。これにより、ジェネリック型パラメータと同等の制約を関連型にも与えられるようになります。

関連型宣言の where

関連型の宣言末尾に where 句を追加し、その関連型が満たすべき条件を記述できます。典型例は、標準ライブラリの Sequence における SubSequence の制約です。

protocol Sequence {
    associatedtype Iterator: IteratorProtocol
    associatedtype SubSequence: Sequence
        where SubSequence.Iterator.Element == Iterator.Element
    // ...
}

ここでは「SubSequence もまた Sequence に適合し、その要素型は自身の要素型と一致する」という不変条件を、関連型の宣言の場所に直接書けます。Sequence に適合する型は、コンパイラによってこの条件の充足が検査されます。

関連型同士の一致(==)だけでなく、「関連型がさらに別のプロトコルに適合する」ような制約も同じ where 句で書けます。

protocol Graph {
    associatedtype Node
    associatedtype Edge where Edge: Hashable
}

プロトコル宣言の where 句から親プロトコルの関連型を参照

プロトコル宣言自体の where 句の中で、親プロトコルの関連型を参照できるようになります。継承したプロトコルの関連型に、さらに絞り込みの制約を重ねるときに使います。

protocol IntSequence: Sequence where Iterator.Element == Int {
    // ...
}

IntSequenceSequence の一種ですが、その Iterator.ElementInt であることがプロトコル側の宣言で固定されます。

名前解決は親プロトコルの関連型に限る

プロトコル宣言の where 句で参照できるのは、あくまで親プロトコル側の関連型です。自分自身が新しく宣言する関連型は、その時点でまだ見えていないため where 句の左辺には書けません。

protocol SomeSequence: Sequence where Counter: SomeProtocol { // error: Use of undefined associated type 'Counter'
    associatedtype Counter
}

自身で宣言する関連型に制約を付けたい場合は、関連型宣言側の where 句を使います。

protocol SomeSequence: Sequence {
    associatedtype Counter: SomeProtocol
}

効果

この変更によって、これまでプロトコル分割やジェネリック関数側への where 句の書き分けで表現していた不変条件を、関連型の宣言そのものの中に閉じ込められるようになります。標準ライブラリでも SequenceCollection の関連型制約がより素直なかたちで記述できるようになっています。