Swift Digest
SE-0346 | Swift Evolution

Lightweight same-type requirements for primary associated types

Proposal
SE-0346
Authors
Pavel Yaskevich, Holly Borla, Slava Pestov
Review Manager
John McCall
Status
Implemented (Swift 5.7)

01 何が問題だったのか

ジェネリクスの利用において、プロトコルに適合する型パラメータの associated type を単一の同値(same-type)要件で固定したい場面は頻繁にあります。しかし、この種の制約は従来 where 句でしか表現できず、読み書きの両面で負担が大きいという問題がありました。

たとえば、String のシーケンスを二つ連結する関数を任意のシーケンス型に一般化しようとすると、次のように書く必要があります。

func concatenate<S: Sequence>(_ lhs: S, _ rhs: S) -> S where S.Element == String {
  ...
}

具象型であれば Array<String> と一言で済むのに対し、プロトコル越しに同じことを言うだけのために型パラメータ S を導入し、where 句で S.Element == String を書く必要があります。単一の same-type 要件しかない単純なケースであっても、この構文的な重さは避けられません。

また、opaque result type の場合はさらに深刻で、そもそも where 句を書く場所がありません。次のように結果型を隠蔽しようとすると、

func readSyntaxHighlightedLines(_ file: String) -> some Sequence {
  ...
}

要素型が [Token] であるという情報が失われ、呼び出し側は結果を Sequence としてしか扱えなくなってしまいます。Array<[Token]> のように要素型を含めて公開する手段が、プロトコル側には用意されていなかったのです。

より根本的には、Swift の学習者にとって Array<Int> のような具象ジェネリック型の構文は自然に受け入れられる一方、Collection の要素型を同じように書き表す手段が無いことは、具象型とジェネリクスの間の不必要な非対称性になっていました。

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

プロトコル側で primary associated type を宣言できるようにし、利用側では Collection<Int> のように具象ジェネリック型と同じ構文で主要な associated type を固定できるようにします。

プロトコル側の宣言

プロトコル名の直後に山カッコで primary associated type のリストを書きます。そこに並べる名前は、プロトコル本体(あるいは継承元)で associatedtype として宣言されている associated type でなければなりません。

protocol Sequence<Element> {
  associatedtype Element
  associatedtype Iterator: IteratorProtocol
    where Element == Iterator.Element
  ...
}

protocol DictionaryProtocol<Key, Value> {
  associatedtype Key: Hashable
  associatedtype Value
  ...
}

primary associated type の役割は「通常は利用側が指定する associated type」を明示することです。Array<Element>Set<Element>Sequence に適合するときに具象型のジェネリックパラメータで埋められるのがまさに Element であり、このような associated type を primary として選ぶのが想定される使い方です。

利用側の構文

primary associated type を持つプロトコルは、プロトコル適合要件を書けるあらゆる位置で、P<Arg1, Arg2, ...> のように型引数を伴って書けます。引数の個数は primary associated type の個数と一致している必要があります。山カッコを省略して従来どおり制約なしで書くこともでき、したがって primary associated type の追加は既存コードに対して source-compatible な変更です。

where 句を使った書き方との対応は次のとおりです。

// extension の対象型
extension Collection<String> { ... }
// 従来: extension Collection where Element == String { ... }

// 別プロトコルの継承節
protocol TextBuffer: Collection<String> { ... }

// ジェネリックパラメータの継承節
func sortLines<S: Collection<String>>(_ lines: S) -> S

// associated type の継承節
protocol Document {
  associatedtype Lines: Collection<String>
}

// where 句内の適合要件の右辺
func merge<S: Sequence>(_ sequences: S)
  where S.Element: Sequence<String>

// opaque parameter(SE-0341)
func sortLines(_ lines: some Collection<String>)

いずれも、T: P<Arg1, Arg2, ...>T: PT.PrimaryType1 == Arg1T.PrimaryType2 == Arg2、…という same-type 要件の組み合わせに展開されます。

ネストした opaque parameter も書けます。

func sort(elements: inout some Collection<some Equatable>) {}
// 従来の書き方では次と等価:
// func sort<C: Collection, E: Equatable>(elements: inout C) where C.Element == E

opaque result type での新しい表現力

opaque result type(some P)でも同じ構文が使えます。これは単なる糖衣ではなく、従来書けなかったことを表現可能にする点が重要です。opaque result type には where 句を付けられないため、戻り値の primary associated type を固定する手段が無かったからです。

func readSyntaxHighlightedLines(_ file: String) -> some Sequence<[Token]> {
  ...
}

外側のスコープのジェネリックパラメータを引数として渡すこともできますし、some のネストも可能です。

func transformElements<S: Sequence<E>, E>(_ lines: S) -> some Sequence<E>

func transform(_: some Sequence<some Equatable>) -> some Sequence<some Equatable>

後者で、引数側と戻り値側の some Sequence<some Equatable> は無関係な別の型である点に注意してください。引数側は呼び出し側が選ぶ型、戻り値側は実装が返す(別の)均質なシーケンスです。

その他の利用位置

次の場所でも同じ構文が使えます。

  • 具象型の継承節。struct Lines: Collection<String> { ... }Element の witness を typealias Element = String で指定するのと同じです。
  • typealias の右辺。typealias SequenceOfInt = Sequence<Int> のように定義して、プロトコル型が書ける位置で利用できます。
  • プロトコル合成の一部。some Sequence<Int> & Equatable のように書けます。

existential は対象外

any Collection<String> のような制約付き existential 型は本Proposalのスコープ外です。型変換や動的キャスト、メタデータまわりの扱いが別途必要になるため、別のProposalで扱われます。

Future Directions

Sequence や Collection など、標準ライブラリのプロトコルに実際に primary associated type を導入する作業は本Proposalの範囲外で、今後の検討課題として残されています。制約付き existential(any Collection<String> など)も、前述のとおり将来的な拡張として位置付けられています。いずれも speculative な見通しであり、本Proposalとしては構文と意味論の土台を整えるところまでが対象です。