構造的opaque result types
Structural opaque result types
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
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 P を structural 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 P が Hashable に適合する保証はどこにもないため、宣言側で明示的に P に Hashable を含めるなどの対応が必要になります。
03 今後の見通し
本提案は、戻り値側のジェネリクス(いわゆる reverse generics、名前付き opaque result type)への自然な足がかりと位置付けられています。
reverse generics が実現すると、たとえば次のように、戻り値の型に名前を付けて複数の戻り値の間で共有したり、その associated type に制約を加えたりできるようになります。
func groupedValues<C: Collection>(in collection: C) -> <Output: Collection> (even: Output, odd: Output)
where C.Element == Int, Output.Element == Int
{
return (even: collection.lazy.filter { $0 % 2 == 0 },
odd: collection.lazy.filter { $0 % 2 != 0 })
}
この構文は強力ですが、associated type に制約を付けたいだけ、というよくあるケースには冗長です。そこで、associated type の制約を山括弧の中に直接書ける軽量な構文や、
func concatenate<T>(a: some Collection<.Element == T>, b: some Collection<.Element == T>) -> some Collection<.Element == T>
さらに簡略化した same-type constraint 構文も検討されています。
func concatenate<T>(a: some Collection<T>, b: some Collection<T>) -> some Collection<T>
本提案で some P を structural position に置けるようにしておくことが、こうした発展の前提となります。あくまで将来の方向性として示されているもので、実現を約束するものではありません。