Allow Generic Types to Abstract Over Packs
01 何が問題だったのか
SE-0393 で導入されたparameter packは、ジェネリック 関数 に対して「可変個の型パラメータを抽象化する」仕組みを与えましたが、struct / class / actor / typealias などの 型宣言 は依然として固定個数のジェネリックパラメータしか持てませんでした。
このため、要素ごとに異なる型を持つコレクションを扱うアルゴリズムを一般化しようとしても、「可変個の型を束ねる返り値の型」を書く手段がありませんでした。たとえば、可変個の Sequence を受け取る variadic な zip 関数を書きたくても、その戻り値の型を表す型名がひとつも存在しません。
func zip<each S>(_ seq: repeat each S) -> ???
where repeat each S: Sequence
現行の標準ライブラリにある Zip2Sequence のように、要素数ごとに Zip3Sequence / Zip4Sequence と並べていくしかなく、SE-0393 で関数シグネチャを variadic 化できても、戻り値になる型側がボトルネックになってしまう状況でした。
02 どのように解決されるのか
ジェネリック関数と同じ each 構文を、struct / class / actor / typealias のジェネリックパラメータリストでも使えるようにします。これにより、型パラメータパックを持つ「variadic な型」を宣言でき、stored propertyの型にパック展開を含められるようになります。
これで、先ほどの variadic な zip の戻り値を表す型を、次のように自分で定義できます。
struct ZipSequence<each S: Sequence>: Sequence {
typealias Element = (repeat (each S).Element)
let seq: (repeat each S)
func makeIterator() -> Iterator {
return Iterator(iter: (repeat (each seq).makeIterator()))
}
struct Iterator: IteratorProtocol {
typealias Element = (repeat (each S).Element)
var iter: (repeat (each S).Iterator)
mutating func next() -> Element? {
return ...
}
}
}
func zip<each S>(_ seq: repeat each S) -> ZipSequence<repeat each S>
where repeat each S: Sequence
以下、今回認められる variadic な型の使い方と制限を整理します。対象は struct / class(actor を含む)/ typealias で、enum と variadic なクラスの継承関係は別提案で扱われます。
型パラメータパックは1つまで
1つの型宣言が直接持てる型パラメータパックは最大1つです。パック以外の普通のパラメータは何個でも混在できます。
struct S1<each T> {} // OK
struct S2<T, each U> {} // OK
struct S3<each T, U> {} // OK
struct S4<each T, each U> {} // error: 型宣言は複数の型パラメータパックを持てない
ただし、variadic な型を入れ子にすれば、実質的に複数のパックを扱えます。
struct Outer<each T> {
struct Inner<each U> {
var fn: (repeat each T) -> (repeat each U)
}
}
型の適用
variadic な型に引数を渡すときは、パック以外のパラメータが先頭と末尾の「固定長の部分」となり、残りがパックに束ねられます。
struct S<T, each U, V> {}
S<Int, Float>.self // T = Int, U = Pack{}, V = Float
S<Int, Bool, Float>.self // T = Int, U = Pack{Bool}, V = Float
S<Int, Bool, String, Float>.self // T = Int, U = Pack{Bool, String}, V = Float
S<Int>.self // error: 最低2個の引数が必要
U には空のパックも代入できます。ジェネリックパラメータが「パックのみ」の場合、引数を空にした V< > という表記でパックに空を代入できます。同じ型名を引数なしで書く V とは意味が異なり、V は「パックが制約されていない状態」を表し、型推論可能な位置や、V 自身の本体・extension 内(Self と同義)でのみ使えます。
struct V<each T> {}
V< >.self // T = Pack{}
V // 制約なし。推論文脈でのみ
プレースホルダ _ は常に「パックの1要素分」として解釈されます。したがって V<_> は要素1個のパックを、V<_, _> は要素2個のパックを意味します。
let x: V<_> = V<Int>() // OK
let x: V<_, _> = V<Int, String>() // OK
let x: V<_> = V<Int, String>() // error
stored property
variadic な型の stored property の型には、パック展開を含められます。ただし、stored property の型 そのもの をパック展開型にすることはできず、パック展開はタプル型・関数型・他の variadic な型の引数といった「ネストされた位置」にのみ書けます。
struct S<each T> {
var a: (repeat each Array<T>) // タプルの中
var b: (repeat each T) -> (Int) // 関数型のパラメータ位置
var c: Other<repeat each T> // 他のvariadic型の引数
}
これは関数パラメータがパック展開型を直接取れるのと対照的で、将来的に「stored property pack」として制限が緩められる可能性があります(speculative)。
制約の推論
パック付きジェネリック型に制約を付けると、関数側の推論でもその制約が自動的に展開されます。たとえば、
protocol P { associatedtype A }
struct ImposeRequirement<T> where T: P {}
struct ImposeRepeatedRequirement<each T> where repeat each T: P {}
// 'repeat each U: P' が推論される
func demonstrate1<each U>(_: repeat ImposeRequirement<each U>)
// 'Int: P, V: P, repeat each U: P' が推論される
func demonstrate2<each U, V>(_: ImposeRepeatedRequirement<Int, V, repeat each U>)
パックをまたぐ複雑な同型要件(異なる深さの展開に属する複数のパック要素が現れる形)は、現行の言語で表現できる要件の範囲を超えるため、エラーとなります。
プロトコル適合と type alias
variadic な struct / class / actor はプロトコルに適合でき、associatedtype の要件は「パック展開を含む typealias」で満たせます。variadic な typealias 自体も、独自のパックを持つか外側の variadic 型にネストされていれば宣言でき、stored property と同じくパック展開は入れ子位置のみに書けます。
typealias Element = (repeat (each S).Element)
typealias Callback = (repeat each S) -> ()
typealias Factory = Other<repeat each S>
typealias は他の typealias と同様、ジェネリック関数の中にも書けます(struct / class は従来どおり関数内には書けません)。
クラスの継承に関する制限
variadic なクラスは非 final でも宣言できますが、他のクラスのスーパークラスにはできません。オーバーライド検査やイニシャライザ継承のルールは別提案で詰める方針で、いまは利用時点でエラーにする形になっています。
class Base<each T> {
func foo(t: repeat each T) {}
}
// error: 型パラメータパックを持つクラスは継承できない
class Derived<U, V>: Base<U, V> {
override func foo(t: U, _: V) {}
}
ABIに関する注意
variadic な typealias は実行時サポートを必要としませんが、それ以外の variadic 型は新しいランタイムエントリポイントを使うため、古いランタイムへのバックデプロイはサポートされません。また、既存の固定個数ジェネリック型を variadic 型に差し替える変更はバイナリ互換性を壊すため、ABI 安定なフレームワークでは別のシンボルとして新規導入する必要があります。
Future Directions(speculative)
今回のスコープから外された項目として、以下が将来の提案で扱われる想定です。いずれも実現を約束するものではありません。
- variadic な
enum - variadic なクラスの継承(オーバーライド・イニシャライザ継承のルール整備)
- stored property pack(タプルでラップせずにパック展開を直接stored propertyにできるようにする)