Opaque Parameter Declarations
01 何が問題だったのか
Swift のジェネリクス構文は表現力を優先して設計されており、関数の入力・出力のあいだに複雑な制約を張り巡らせることができます。たとえば次の関数は、二つの Sequence を受け取り、要素型が一致するときだけ連結して配列を返します。
func eagerConcatenate<Sequence1: Sequence, Sequence2: Sequence>(
_ sequence1: Sequence1, _ sequence2: Sequence2
) -> [Sequence1.Element] where Sequence1.Element == Sequence2.Element
このような高度な制約を書きたい場面では、ジェネリクスパラメータリストと where 句の組み合わせは必要不可欠です。一方で、そこまで複雑な制約が必要ない場合でも同じ構文を使う必要があるため、「単にある protocol に適合する値を受け取りたいだけ」の関数までもがボイラープレートで重たく見えてしまいます。
たとえば SwiftUI で二つのビューを横に並べる関数を書くと、次のようになります。
func horizontal<V1: View, V2: View>(_ v1: V1, _ v2: V2) -> some View {
HStack {
v1
v2
}
}
V1 と V2 はそれぞれ一度しか使われず、名前を与える必要も特にありません。「View を二つ受け取って View を返す」だけの関数なのに、宣言を読み解くにはジェネリクスパラメータリストに目を通す必要があり、関数の本質より構文上の飾りのほうが目立ってしまいます。
戻り値の側では、SE-0244 で導入された opaque result type(some View のような書き方)によって、具体的な型を名前で書かずに「View に適合する何らかの型」とだけ述べられるようになっていました。ところがパラメータ側には同じショートカットがなく、同じように「名前は不要で、制約だけ述べたい」ケースでも、従来どおりジェネリクスパラメータリストを書く必要がありました。
02 どのように解決されるのか
本Proposalは、some キーワードを関数・イニシャライザ・サブスクリプトのパラメータ型でも使えるようにします。some P はパラメータ位置では「P に適合する、名前のないジェネリクスパラメータ」を表し、コンパイラが暗黙のジェネリクスパラメータに展開します。
先ほどの horizontal は、ジェネリクスパラメータリストを書かずに次のように表現できます。
func horizontal(_ v1: some View, _ v2: some View) -> some View {
HStack {
v1
v2
}
}
意味的には、次のジェネリクス関数とまったく同じです。some View の出現ごとに別々の暗黙のジェネリクスパラメータが生成されます。
func horizontal<_V1: View, _V2: View>(_ v1: _V1, _ v2: _V2) -> some View {
HStack {
v1
v2
}
}
パラメータ側の some は呼び出し側が型を決める
戻り値側の opaque result type(-> some P)と、パラメータ側の opaque parameter type(some P)は、似た構文ですが型を決める主体が異なります。戻り値側は関数の実装側(callee)が具体的な型を決めて、呼び出し側(caller)はそれを名前で知ることができません。一方、パラメータ側の some P はこれまでのジェネリクスパラメータと同じく、呼び出し側が渡した引数から型推論で決まります。
func f(_ p: some P) { }
f(17) // some P は Int に推論される
f("Hello") // some P は String に推論される
let fInt: (Int) -> Void = f // Int に推論される
let fString: (String) -> Void = f // String に推論される
let fAmbiguous = f // error: some P の型が決まらない
構造的な位置にも書ける
SE-0328 で戻り値型のあらゆる構造的な位置に some P を書けるようになったのと同じく、パラメータでも配列やジェネリクス型の型引数の中などに some を入れられます。
func encodeAnyDictionaryOfPairs(
_ dict: [some Hashable & Codable: Pair<some Codable, some Codable>]
) -> Data
これは次と等価で、some の出現ごとに別々の暗黙のジェネリクスパラメータが入ります。
func encodeAnyDictionaryOfPairs<_T1: Hashable & Codable, _T2: Codable, _T3: Codable>(
_ dict: [_T1: Pair<_T2, _T3>]
) -> Data
使える場所の制限
opaque parameter type が書けるのは、関数・イニシャライザ・サブスクリプトのパラメータだけです。型エイリアスや関数型の値(変数の型注釈など)では使えません。
typealias Fn = (some P) -> Void // error: typealias では opaque type を使えない
let g: (some P) -> Void = f // error: 関数型の値でも使えない
また、可変長パラメータ(some P...)にも書けません。将来 variadic generics が入ったときに「すべての引数が同じ型でなければならない」のか「引数ごとに型が違ってよい」のかが変わる可能性があり、意味を先取りしてしまわないよう現時点では禁止されています。
func acceptLots(_: some P...) // error
さらに、関数型の引数の「consuming ポジション」(関数型パラメータのパラメータ側)でも使えません。
func g(fn: (some P) -> Void) { } // error
これは、g の実装側から見たときに some P 型の値を自分で作り出す手段がなく、fn を呼ぶことができないためです。同じ理由で、戻り値として返される関数型のパラメータ位置にも書けません(-> (some P) -> Void は不可)。
従来のジェネリクス構文と組み合わせる
opaque parameter type はジェネリクスパラメータリストを完全に置き換えるものではなく、あくまで「名前が要らない場合のショートカット」です。異なる some P の出現は別々の暗黙のパラメータになるため、「二つの引数の型を等しくそろえたい」「要素型を戻り値でも使いたい」といった場合はこれまでどおり名前付きのジェネリクスパラメータを使います。両者は同じ関数内で混在させることもできます。
今後の方向性
以下は speculative な見通しで、実現を約束するものではありません。
protocol の関連型をジェネリクス構文で制約する記法(Collection<String> を「Element が String の Collection」と読む書き方)と組み合わせると、opaque parameter type の表現力はさらに広がります。冒頭の eagerConcatenate は、要素型だけを一つのジェネリクスパラメータで表し、二つのシーケンスを some Sequence<T> で受けるだけのシンプルな形に書き直せるようになることが期待されています。
func eagerConcatenate<T>(
_ sequence1: some Sequence<T>, _ sequence2: some Sequence<T>
) -> [T]
戻り値側の opaque result type と組み合わせれば、戻り値の具体的な型も隠せます。
func lazyConcatenate<T>(
_ sequence1: some Sequence<T>, _ sequence2: some Sequence<T>
) -> some Sequence<T>
また、現時点で禁止されている「consuming ポジション」の opaque type も、呼び出し側と実装側のどちらが型を決めるかを反転させる意味論を与えれば将来的に解禁される可能性があります(g(fn: (some P) -> Void) であれば、g の実装側が型を選び、呼び出し側は任意の T: P に対応できるクロージャを渡す、という形です)。