Swift Digest
SE-0157 | Swift Evolution

Support recursive constraints on associated types

Proposal
SE-0157
Authors
Douglas Gregor, Erica Sadun, Austin Zheng
Review Manager
John McCall
Status
Implemented (Swift 4.1)

01 何が問題だったのか

プロトコルの関連型(associated type)には、これまで「別のプロトコルに適合する」という継承風の制約や、SE-0142 で追加された where 句による制約を書けました。しかし、制約の中で、現在定義中のプロトコル自身(あるいはそれに依存するプロトコル)を参照することはできませんでした。これを再帰的な制約(recursive constraint)といいます。

書きたくても書けなかった制約

典型例は SequenceSubSequence です。「部分列もまた Sequence である」というのは自然な不変条件ですが、次の宣言はコンパイルが通りませんでした。

// 従来はコンパイルできなかった
protocol Sequence {
    associatedtype SubSequence: Sequence
        where Iterator.Element == SubSequence.Iterator.Element,
              SubSequence.SubSequence == SubSequence

    func dropFirst(_ n: Int) -> Self.SubSequence
    // ...
}

SubSequence: Sequence の部分が、定義しようとしている Sequence 自身への再帰的な参照になっているためです。

制約を諦めてドキュメントや利用側に逃がすしかなかった

この制限のため、本来プロトコル側で保証したい条件を宣言の外へ押し出す必要がありました。関連型には制約を付けず、代わりに「部分列も Sequence であるべきで、要素型は同じで、その部分列自身も自分と同じ型になるべき」といった取り決めをコメントで書き、各利用箇所の where 句で改めて制約を課すという回避策です。

protocol Sequence {
    // SubSequenceはSequenceに適合しているべき。
    // SubSequenceの要素型は、このシーケンスの要素型と一致するべき。
    // SubSequenceのSubSequenceは自分自身であるべき。
    associatedtype SubSequence

    func dropFirst(_ n: Int) -> Self.SubSequence
    // ...
}

結果として、コードは冗長になり、意図もぼやけ、標準ライブラリ側ですら条件を表現するために _Indexable_BidirectionalIndexable といったアンダースコア付きの補助プロトコルを切り出してしのぐ必要がありました。

利用側が誤った実装をしても検出できなかった

関連型の不変条件がプロトコルに書き切れないということは、コンパイラが違反を検出できないということでもあります。たとえば RangeReplaceableCollection に適合する型の SubSequenceRangeReplaceableCollection でない、という意味的に壊れた実装も、プロトコル側で縛れないため通ってしまう状況でした(実際、Foundation の Data が一時期この不変条件を破っていました)。

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

関連型の制約における再帰的な参照の禁止を撤廃します。これにより、関連型の制約の中で、定義中のプロトコル自身や、それに依存する別のプロトコルを自由に参照できるようになります。利用者から見ると「これまで弾かれていた宣言が通るようになる」だけのシンプルな変更です。

再帰的な制約が書けるようになる

SequenceSubSequence は、ようやく意図どおりの宣言ができるようになります。

protocol Sequence {
    associatedtype SubSequence: Sequence
        where Iterator.Element == SubSequence.Iterator.Element,
              SubSequence.SubSequence == SubSequence

    func dropFirst(_ n: Int) -> Self.SubSequence
    // ...
}

これで「部分列もまた Sequence であり、要素型は元のシーケンスと同じで、部分列の部分列は自分自身である」という不変条件がプロトコル定義の中に収まります。Sequence に適合する側は、これらの条件を満たすかどうかをコンパイラに検査してもらえます。

標準ライブラリの整理

この変更を前提に、コレクション系プロトコルの関連型定義が整理されます。主な変更は次の通りです。

  • Sequence: associatedtype SubSequence: Sequence where Iterator.Element == SubSequence.Iterator.Element, SubSequence.SubSequence == SubSequence
  • Collection: associatedtype SubSequence: Collection where SubSequence.Index == Indexassociatedtype Indices: Collection where Indices.Iterator.Element == Index, Indices.Index == Index
  • BidirectionalCollection: SubSequenceIndicesBidirectionalCollection に制約
  • RandomAccessCollection: SubSequenceIndicesRandomAccessCollection に制約
  • MutableCollection: SubSequenceMutableCollection に制約
  • RangeReplaceableCollection: SubSequenceRangeReplaceableCollection に制約
  • Arithmetic: associatedtype Magnitude: Arithmetic

またこの整理に合わせて、これまで回避策として使われていた _Indexable / _BidirectionalIndexable などのアンダースコア付き補助プロトコルが取り除かれ、IndexingIteratorDefault*Indices*Slice 系の宣言もそれぞれのコレクションプロトコルを直接要求する形に書き換えられます。

利用側コードへの影響

ほとんどのユーザコードにとっては、これは純粋な機能追加です。ジェネリックコードを書くときに、これまで where 句で重ねて書く必要があった制約(例えば「C.SubSequence: Collection で、その IndexC.Index と等しい」といった条件)が、プロトコル側の宣言によって自動的に保証されるようになるため、制約の記述が短くなります。

一方、関連型の不変条件に違反していた独自の型定義は、新しい制約によって弾かれるようになります。たとえば RangeReplaceableCollection に適合する型の SubSequence を、RangeReplaceableCollection でない型にしているような定義は、今後はコンパイルエラーになります。これはコードが壊れたというより、コンパイラがプロトコルの不変条件を正しく検査できるようになった結果です。