Swift Digest
SE-0328 | Swift Evolution

Structural opaque result types

Proposal
SE-0328
Authors
Benjamin Driscoll, Holly Borla
Review Manager
Ben Cohen
Status
Implemented (Swift 5.7)

01 何が問題だったのか

opaque result type(some P)を導入した SE-0244 では、「関数の戻り値の型」「変数の型」「subscript の戻り値の型」に some P を書けるようにしましたが、その型そのものが丸ごと opaque でなければならないという制限がありました。つまり、some P型全体の先頭に1つだけ置ける、という形でしか使えませんでした。

この制限のために、以下のような自然な API 宣言がそのまま書けません。

protocol P {}
protocol Q {}
struct S<T> {}

// ❌ opaque な値を Optional で包んで返したい(失敗し得る関数)
func f0() -> (some P)? { /* ... */ }

// ❌ opaque な値を複数タプルで返したい
func f1() -> (some P, some Q) { /* ... */ }

// ❌ 遅延計算される opaque な値を、クロージャとして返したい
func f2() -> () -> some P { /* ... */ }

// ❌ より一般に、opaque な値をジェネリック型の中に埋め込んで返したい
func f3() -> S<some P> { /* ... */ }

これらはいずれも「戻り値の一部が opaque である」ケースです。opaque result type の本来の狙い(内部の具体型を隠しつつ、呼び出し側には「ある P に適合する型」として使わせる)から見ると、こうしたパターンこそ書けてほしいものですが、SE-0244 時点の構文ではトップレベルの位置しか許されておらず、上記はすべてコンパイルエラーになっていました。

この制限は意味論的に必要だったわけではなく、単に構文上 some P をどこに置けるかを絞っていただけです。そのため、素直に書けない分だけ API 表現力が損なわれていました。

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

「関数の戻り値の型」「変数の型」「subscript の戻り値の型」において、opaque result type some Pstructural position(構造の内部の位置)にも書けるようにします。これにより、先ほど書けなかった宣言がそのまま通るようになります。

protocol P {}
protocol Q {}
struct S<T> {}

func f0() -> (some P)? { /* ... */ }      // ✅
func f1() -> (some P, some Q) { /* ... */ } // ✅
func f2() -> () -> some P { /* ... */ }    // ✅
func f3() -> S<some P> { /* ... */ }       // ✅

ただし、いくつか注意すべきルールがあります。

Optional と組み合わせるときの書き方

some キーワードは ?! よりも弱く結合します。そのため、opaque な型の Optional を書きたいときは、必ず括弧でくくる必要があります。

func f() -> (some P)?   // ✅ 「Optional<some P>」の意味
func g() -> (some P)!   // ✅ 「ImplicitlyUnwrappedOptional<some P>」の意味

一方、some P? と書くと some Optional<P> の意味に解釈されてしまいます。opaque result type の制約には「Any / AnyObject / プロトコル合成 / 基底クラス」しか書けないため、Optional<P> を制約にすることはできず、エラーになります。some P! も同様です。

コンパイラはこの誤りに対して some P?(some P)? への fix-it を提示します。

関数型を返す場合、some は戻り位置にしか書けない

関数の戻り値の型・変数の型・subscript の戻り値の型が関数型であるとき、その関数型の中で some を使えるのは 戻り位置 に限られます。引数位置 には書けません。

// ✅ 返ってきた関数 () -> some P を呼び出して opaque な値を得られる
func f() -> () -> some P { /* ... */ }

// ❌ error: 'some' cannot appear in parameter position in result type '(some P) -> ()'
func g() -> (some P) -> () { /* ... */ }

これは意味論的にも自然な制約です。もし (some P) -> () のような型を返せてしまうと、呼び出し側はその opaque な引数型に「具体的に何を渡せばよいか」が分からず、結局そのクロージャを呼び出せません。また、SE-0341(opaque parameter declarations)は引数位置の some に別の意味(ジェネリックパラメータの略記)を与えているため、関数型の引数位置で some を許すと読み手を混乱させます。

ジェネリックの制約推論は行われない

通常のジェネリックパラメータは、戻り値の型のどこで使われているかに応じて制約が暗黙に推論されます。

struct H<T: Hashable> { init(_ t: T) {} }

// 'H<T>' の中に T が使われているので、暗黙に 'T: Hashable' が入る
func f<T>(_ t: T) -> H<T> {
    var h = Hasher()
    h.combine(t) // OK: T は Hashable と分かっている
    _ = h.finalize()
    return H(0)
}

一方、opaque result type では このような制約推論は行われませんsome P の「underlying type」は関数の実装が決めるもので、宣言側からは P という明示的な制約しか見えない、というのが opaque result type のモデルだからです。そのため、次のようなコードはエラーになります。

// ❌ error: type 'some P' does not conform to protocol 'Hashable'
func f<T>(_ t: T) -> H<some P> { /* ... */ }

H<T>T: Hashable を要求しますが、some PHashable に適合する保証はどこにもないため、宣言側で明示的に PHashable を含めるなどの対応が必要になります。

今後の方向性

この拡張は、いわゆる reverse generics(戻り値側のジェネリクス、名前付き opaque result type)への自然な足がかりと位置付けられています。例えば将来的に、associated type に制約を付けた形(some Collection<.Element == T> のような軽量 same-type constraint 構文)や、複数の opaque 戻り値に共通の型名を与える構文が提案され得ます。本提案はそうした発展の前段として、まず some P を構造の内部に置けるようにする、という位置づけです(あくまで将来の方向性であり、実現を約束するものではありません)。