Lightweight same-type requirements for primary associated types
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: P と T.PrimaryType1 == Arg1、T.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としては構文と意味論の土台を整えるところまでが対象です。