SE-0352: Implicit Opened Existentials
![Ueeek](https://res.cloudinary.com/zenn/image/fetch/s--NP7Uoaam--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/c0a862dd95.jpeg)
Introduction
Swiftにおける存在型はその具体的な型が分っていない変数を代入することのできる型で、代入する型は実行時に動的に変わる可能性がある。
代入された変数の動的な型は
格納された値の動的型 (existential's underlying type) は、それが準拠するプロトコルのセットと、場合によってはそのスーパークラスによってからしか分らない。
存在型は動的な型をもつ変数を表現するために便利であるいっぽう、その動的な性質のせいで制限されている。
(SE-03335)のプロポーザルで開発者はよりその動的な性質に理解を深めた。同時に存在型とGenericsを組み合わせて使用することが非常に難しいことも。
開発者はよく、そのことを以下のエラーメッセージが表示されることを遭遇する。
protocol P as a type cannot conform to itself
protocol P {
associatedtype A
func getA() -> A
}
func takeP<T: P>(_ value: T) { }
func test(p: any P) {
takeP(p) // error: protocol 'P' as a type cannot conform to itself
}
この動作は存在型の使用における罠になっている。
Genericsの代りに存在型を使用することは簡単だが、一度存在型を使用してしまうと、 Genericsを使用する方法に戻るのはとても難しくなる。
最悪なことに、多くの関数のパラメータや戻り値をany P
からgenricsのP
に変更したりする必要がでてきる。
このプロポーザルは上記の問題への解決策を提案する。存在型をGenericsと一緒に使いやすくする?
そうすることで、存在型のGenerics型を持つ関数を呼び出すことができるようになり、そのようなGenerics関数では Exsistential box自身ではなくExistential's underlying typeを操作するようになる。
それによって、存在型の困難だった点を大きな Refactorginをすることなく取り除くことができる。
この機能は存在型の変数にアクセスするときにはすでに存在し、このプロポーザルでその機能をほとんど目に見えない方法ですべての呼び出し引数に拡張する。
これまで失敗していたGenerics関数呼び出しも成功するようになる。
存在型とGenericsを組み合わせて使うことをより簡単にすることで、コードをよりシンプルに記述できるようになり、より扱いやすい言語になる。
![Ueeek](https://res.cloudinary.com/zenn/image/fetch/s--NP7Uoaam--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/c0a862dd95.jpeg)
Proposed solution
存在型からGenericsに戻ることをより簡単にするために、存在型がGenericsTypeのパラメータに渡されているときに、存在型を暗黙的に"open"することを提案する。
そのような場合では、Generics引数はExistential Boxではなく、存在型のUnderlying型を参照する。
Protocol Costume
を例にする。 (引数や戻り値にSelf
を持っている) そして、CostumeのプロパティをチェックするGenerics関数を書く。
protocol Costume {
func withBells() -> Self
func hasSameAdornments(as other: Self) -> Bool
}
// Okay: generic function to check whether adding bells changes anything
func hasBells<C: Costume>(_ costume: C) -> Bool {
return costume.hasSameAdornments(as: costume.withBells())
}
これは大丈夫である。しかし、全てのCostume
がベルを持っているかどうかを確認する関数を書いてみる。
すると、存在型の配列と、Generic Functionの間で問題が発生する
func checkFinaleReadiness(costumes: [any Costume]) -> Bool {
for costume in costumes {
if !hasBells(costume) { // error: protocol 'Costume' as a type cannot conform to the protocol itself
return false
}
}
return true
}
hasBElls
の呼び出しでは、Generics parameter(C
)は any Costume
型(未知のunderlying typeを持つbox)にバインドされる。
それぞれの box Typeのインスタンスは、実行時に異なる型を持っているかもしれない。つまり、Underlying TypeはCustume
に準拠しているが、box型はそうでははい。
Box自身は、hasSameAdorments
を持たないため、Custume
に準拠していない。つまり ふたつのboxは同じUnderlying Typeを持っているとは限らない。
この提案では、"Implicitly opened existentials"を導入する。それは、Underlying TypeがGenerics パラメータでキャプチャできる存在型の変数(any Costume
)の使用を可能にする。
例えば、上記のhasBells(costume)
の呼び出しは、Generic Parameter C
をCustome
の特定のインスタンスにバインドすることによって、可能になる。
それぞれの Iterationで、C
には異なるUnderlying Typeがバインドされる。
func checkFinaleReadiness(costumes: [any Costume]) -> Bool {
for costume in costumes {
if !hasBells(costume) { // okay with this proposal: C is bound to the type stored inside the 'any' box, known only at runtime
return false
}
}
return true
}
"Implicitly opening existentials"はGeneric 関数のParameterが、動的に変更される型を受け付けることができ、そのunderlying Typeをパラメータにバインドすることができるようになり、動的に型付けられる変数から静的に型付けられるものに変換されることが可能になる。
これは実は新しいことである。存在型のプロトコルのメンバーを呼びだすときに、暗黙的にSelf
型がOpen(展開?)される。
この機能によって、hasBells
をprotocol extensionの中に書くことができるようになる。
Implicitly opening existentials allows one to take a dynamically-typed value and give its underlying type a name by binding it to a generic parameter, effectively moving from a dynamically-typed value to a more statically-typed one. This notion isn't actually new: calling a member of a protocol on a value of existential type implicitly "opens" the Self
type. In the existing language, one could implement a shim for hasBells
as a member of a protocol extension:
extension Costume {
var hasBellsMember: Bool {
hasBells(self)
}
}
func checkFinaleReadinessMember(costumes: [any Costume]) -> Bool {
for costume in costumes {
if !costume.hasBellsMember { // okay today: 'Self' is bound to the type stored inside the 'any' box, known only at runtime
return false
}
}
return true
}
Generic Functionの呼び出しにおける"Implicitly Opening existential"は 既存の全てのGeneric Parmameterの挙動の一般化と言える。
厳密には、表現力が高くなったわけではない。hasBellsMember
の例で示すように、いつでも、このopening behavior
をprotocol extensionに書くこともできます。
このプロポーザルの目的は、存在型の暗黙的なopen
をより統一的で人間工学的にすることです。
準備段階の最後の例を見ていく。
この例では、bellをチェックするコードを、ロジックを分離したGeneric function hasBells
に入れることなく、open
したい。
func checkFinaleReadinessOpenCoded(costumes: [any Costume]) -> Bool {
for costume in costumes {
let costumeWithBells = costume.withBells() // returned type is 'any Costume'
if !costume.hasSameAdornments(costumeWithBells) { // error: 'any Costume' isn't necessarily the same type as 'any Costume'
return false
}
}
return true
}
ふたつの点に着目したい。
ひとつめは、withBells()
はSelf
を返すことである。
any Costume
型の変数のメソッドを呼び出すとき、具体的な返り値の型は分っていない、そのためany Cosutme
に型消去される。(costumeWithBellsの型)
ふたつめは、hasSameAdornments
は型エラーになる。なぜなら、その関数は引数としてSelf
型を期待しているが、custume
とcustomeWithBells
の間に繋りはなく、どちらもany Costume
型である。
Implicitly Openingは呼び出し時にのみ適応されため、その効果は呼び出し終了時に型消去される。
オープンの効果を複数のステートメントにわたって持続させるには、hasBells と同様に、そのコードをジェネリック パラメーターに名前を与えるジェネリック関数に分割します
![Ueeek](https://res.cloudinary.com/zenn/image/fetch/s--NP7Uoaam--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/c0a862dd95.jpeg)
any
and some
Moving between この提案のある興味深い点は、コードへの影響があまりないように、any
parameterをsome
にリファクタリングできるようになることである。
hasBElls
をsome
を使って書いてみる。
func hasBells(_ costume: some Costume) -> Bool {
return costume.hasSameAdornments(as: costume.withBells())
}
この提案では、asyn Costume
の値を渡して、hasBell
を呼ぶことができる。
func isReadyForFinale(_ costume: any Costume) -> Bool {
return hasBells(costume) // implicit opening of the existential value
}
これまでの挙動として 静的に型付けられたsome Costume
が、any Costume
に変換されることは可能であった。
この提案では、逆向きの変換が可能になった(some → any
)
func isReadyForFinale(_ costume: some Costume) -> Bool {
return hasBells(costume) // okay, `T` binds to the generic argument
}
具体的な型でisReadyForFinale
を呼び出す人は、any Costume
のBoxingによるオーバーヘッドを回避することができる。
any Costume
で呼ぶ人は、暗黙的に存在型がopen
され isReadyForFinale
が呼ばれる。
これにより、すべてのクライアントが一斉にGenericを使うようにするのではなく、段階的に変更できるようになる。
![Ueeek](https://res.cloudinary.com/zenn/image/fetch/s--NP7Uoaam--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/c0a862dd95.jpeg)
Detailed design
基本的に、"存在型を展開する"は、Existential boxを調べて、そこに保存されている動的な型を見つけ、その動的な型に"名前"を付けことである。
その動的な型名は、Generic Parameterにキャプチャされる必要があるため、静的に推論でき、その型の値はGeneric関数の呼び出しに渡すことができる。
そのような呼び出しの結果は、その動的な型名を参照するかもしれない。その場合は、存在型に戻される必要がある。
呼び出し後は、展開された存在型の型名が型システムに漏洩しないように、型消去して存在型に戻す必要がある。
この両方は、既存の言語機能で実現でき、この機能が型システム自体への大きな拡張を構成することを防ぎます。(メンバのひとつにアクセスするときに存在型を展開する)
このセクションでは、"在型の展開"と"存在型へ型消去によって戻すこ"との詳細を記述する。
これらの詳細はユーザに隠蔽されるべきであり、この機能はコードが現在拒否されている場所でGenericsと存在型を使用するためだけの機能として明示される必要がある。
しかし、詳細で述べることはたくさんある、なぜなら動的に型付けられる存在型のboxを静的に型付けられたGenericに変換することは、型のidentityを評価について注意深く扱う必要がある。
![Ueeek](https://res.cloudinary.com/zenn/image/fetch/s--NP7Uoaam--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/c0a862dd95.jpeg)
When can we open an existential?
存在型を展開するためには、引数は 存在型であるか、存在型のメタタイプである必要がある。そして、存在型のunderlying typeに直接バインドできるGeneric Parameterに関連した型パラメータに与えられる必要がある。
これは、例えば、存在型は、そのunderlying TypeがGeneric Parameterに直接バインドできるときに展開されると言うこともできる。
protocol P {
associatedtype A
func getA() -> A
}
func openSimple<T: P>(_ value: T) { }
func testOpenSimple(p: any P) {
openSimple(p) // okay, opens 'p' and binds 'T' to its underlying type
}
また、input
parameterを展開することも可能である。Generic functionはそのunderlying typeを扱い、mutating
methodを呼ぶことができる。しかし、existential boxにアクセスできるわけではないので、その動的な型を変えることはできない。
func openInOut<T: P>(_ value: inout T) { }
func testOpenInOut(p: any P) {
var mutableP: any P = p
openInOut(&mutableP) // okay, opens to 'mutableP' and binds 'T' to its underlying type
}
しかし、ふたつ以上の存在型がある場合かひとつもない場合は展開することができない。
なぜなら、推論するためにはunderlying typeがひとつであると保証する必要があるからである。
Generic parameterが複数箇所で使用されているために、存在型の展開が起きない例
func cannotOpen1<T: P>(_ array: [T]) { .. }
func cannotOpen2<T: P>(_ a: T, _ b: T) { ... }
func cannotOpen3<T: P>(_ values: T...) { ... }
struct X<T> { }
func cannotOpen4<T: P>(_ x: X<T>) { }
func cannotOpen5<T: P>(_ x: T, _ a: T.A) { }
func cannotOpen6<T: P>(_ x: T?) { }
func testCannotOpenMultiple(array: [any P], p1: any P, p2: any P, xp: X<any P>, pOpt: (any P)?) {
cannotOpen1(array) // each element in the array can have a different underlying type, so we cannot open
cannotOpen2(p1, p2) // p1 and p2 can have different underlying types, so there is no consistent binding for 'T'
cannotOpen3(p1, p2) // similar to the case above, p1 and p2 have different types, so we cannot open them
cannotOpen4(xp) // cannot open the existential in 'X<any P>' there isn't a specific value there.
cannotOpen5(p1, p2.getA()) // cannot open either argument because 'T' is used in both parameters
cannotOpen6(pOpt) // cannot open the existential in '(any P)?' because it might be nil, so there would not be an underlying type
}
cannotOpen6(pOpt)
が展開できないのは、引数がnil
になりうるからである。
厳密には、Optional
の引数を展開することはできるが
cannotOpen6(p1) // we *could* open here, binding T to the underlying type of p1, but choose not to
呼び出されているのに展開しないという妙な動作になるために、この提案ではそれを可能にしない。
存在型のメタタイプは展開可能であるが、制約がある。
func openMeta<T: P>(_ type: T.Type) { }
func testOpenMeta(pType: any P.Type) {
openMeta(pType) // okay, opens 'pType' and binds 'T' to its underlying type
}
![Ueeek](https://res.cloudinary.com/zenn/image/fetch/s--NP7Uoaam--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/c0a862dd95.jpeg)
Type-erasing resulting values
Generic 関数の返り値はGeneric Parameterとassociatedtypeに関連している必要がある。
例えば、下記は、Original Valueとassociated型のひとつをReturnするGeneric 関数である。
protocol Q {
associatedtype B: P
func getB() -> B
}
func decomposeQ<T: Q>(_ value: T) -> (T, T.B, T.B.A) {
(value, value.getB(), value.getB().getA())
}
存在型でdecomposeQ
を呼び出したときは、存在型は展開され、そのunderlying typeはT
にバインドされる。
T.B
とT.B.A
はunderlying typeから得られる型である。
すべてが完了すると、T
,T.B
,T.B.A
は上限まで型消去される。(すべての要件をキャプチャする存在型まで?)
func testDecomposeQ(q: any Q) {
let (a, b, c) = decomposeQ(q) // a is any Q, b is any P, c is Any
}
これは、SE-0309と全く同じであり、同一のルールがここでも適用される。
そのルールをここで、任意のGeneric parameterに一般化した制約を再掲する
Generic Parameter T
を展開された存在型にバインドするときに、T
と以下の条件を満すT-rooted accosiated types
- 具体的な型にバインドされてない
- Generic 関数の返り値の中の、covariant positionに出現する
は、メンバアクセスに使用される存在型のGeneric Signatureに従って,その上限の型まで型消去される。
T
かT-rooted associatedtype
が返り値の型のnon-covariant 位置に出現する場合は、型消去の結果を表現する方法がないので、T
は存在型のunderlying typeにバインドされない。
これは、存在型の展開を防ぐparameter 型で述べたように必要不可欠なものである。
func cannotOpen7<T: P>(_ value: T) -> X<T> { /*...*/ }
返り値は存在型への型消去が許可されているため、Optional, tuples, arrayも許可される。
func openWithCovariantReturn1<T: Q>(_ value: T) -> T.B? { /*...*/ }
func openWithCovariantReturn2<T: Q>(_ value: T) -> [T.B] { /*...*/ }
func covariantReturns(q: any Q){
let r1 = openWithCovariantReturn1(q) // okay, 'T' is bound to the underlying type of 'q', resulting type is 'any P'
let r2 = openWithCovariantReturn2(q) // okay, 'T' is bound to the underlying type of 'q', resulting type is '[any Q]'
}
![Ueeek](https://res.cloudinary.com/zenn/image/fetch/s--NP7Uoaam--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/c0a862dd95.jpeg)
"Losing" constraints when type-erasing resulting values
展開された存在型を伴った呼び出しの結果が型消去されたときに、返り値の型の情報がすべては表現されない可能性がある。(Upper boundに型消去することで情報が失われる)
たとえば、b
の型を考える
protocol P {
associatedtype A
}
protocol Q {
associatedtype B: P where B.A == Int
}
func getBFromQ<T: Q>(_ q: T) -> T.B { ... }
func eraseQAssoc(q: any Q) {
let b = getBFromQ(q)
}
T.B
の型消去が実行されるときに、その上限の型はA == Int
となるP
に除去する型である。しかし、存在型はそのような表現をすることができずb
はany P
となる、
Swiftの存在型は進歩し続けている。
Se-0353がもし採択されたなら、より詳細なupper boundの型を表現できるようになるだろう。
// Assuming SE-0353...
protocol P<A> {
associatedtype A
}
// ... same as above ...
このときb
はany P<Int>
と表現されることになる。
将来的には、存在型の機能が強化されることで、コードの変更なしでより詳細なupper-boundの型が利用できるようになり、暗黙的に展開された存在型を伴う関数呼び出しのあとの型消去がより正確になることが期待できる。
しかし、このような変更はソースコード compatibilityの問題を発生される。オーバーロードなどにより、より制度の低いany P
であるb
の型に依存するようになった可能性があるからである。
func f<T: P>(_: T) -> Int { 17 }
func f<T: P>(_: T) -> Double where T.A == Int { 3.14159 }
// ...
func eraseQAssoc(q: any Q) {
let b = getBFromQ(q)
f(b)
}
より曖昧なupper bound(any P
)のときに、f(b)
の呼び出しはInt
を返すoverloadを選択することになるだろう。
より詳細なupper boud(any P where A===Int
)のときに、f(b)
はDouble
を返すoverloadを選択することになるだろう。
overloadのせいで、次のmajor versionまでupper boundへの変更を延期すること以外で、互換性への影響を完全に取り除くことはできない。
しかし、存在型への制限により upper boundの型を正確に表現できないときに、特定の型の強制を要求することで影響を軽減することを提案する。
getBFromQ(q)
の呼び出しは下記のようにする必要がある
getBFromQ(q) as any P
こうすることで 言語の機能の進化で存在型の表現力があがることでupper boundの型が変化しても、結果としては同じ型(any P)
を生成する。
開発者はSwiftが存在型のすべての情報をキャプチャしていることろではas any P
を安全に取り除くことができる。
明示的な型強制のこの要件は、この提案より前から存在していたものも含め、存在型を展開することによる型消去のすべての場合に適用される。
たとえば、getBFromQ
はprotocol extensionに書くことができる。
以下のコードには、最初に SE-0309 で整形式化されたため、この例と同じ問題 (および同じ解決策) があります。
extension Q {
func getBFromQ() -> B { ... }
}
func eraseQAssocWithSE0309(q: any Q) {
let b = q.getBFromQ()
}
![Ueeek](https://res.cloudinary.com/zenn/image/fetch/s--NP7Uoaam--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/c0a862dd95.jpeg)
Contravariant erasure for parameters of function type
Generic関数の返り値にCoveriant erasureが適応される一方、Generic関数の他のパラメータには逆が適応される。
これは、展開された存在型にバインドされているGenericパラメータを参照する関数型のパラメータに影響がある、それはupper boundsに型消去される。
func acceptValueAndFunction<T: P>(_ value: T, body: (T) -> Void) { ... }
func testContravariantErasure(p: any P) {
acceptValueAndFunction(p) { innerValue in // innerValue has type 'any P'
// ...
}
}
返り値に適応されるcovariant 型消去と同様に、この型消去も、動的な型に付けられた"名前"はclosureパラメータの推論を通して型システムに漏洩する ということはないと保証されている。
それは、Generic パラメータT
が、any P
にバインドされているという錯覚を効果的に維持するが、実際には、特定の値のunderlying typeにバインドされる。
これはルールの例外である。
そのようなパラメータへの引数がGeneric functionを参照しているなら、型消去は発生しない。
そのような場合、動的な型の名前は、ふたつめのGeneric functionのGeneric parameterに直接バインドされる、つまり、同じような暗黙的な存在型の展開を効果的に再実行している。
これは例にするとわかりやすい
func takeP<U: P>(_: U) -> Void { ... }
func implicitOpeningArguments(p: any P) {
acceptValueAndFunction(p, body: takeP) // okay: T and U both bind to the underlying type of p
}
この挙動はほとんどの、_openExistential
の隠れた挙動を包含しており、特にひとつの存在型を展開しそれをGeneric関数に渡すことのみをサポートしている。
_openExistential
は準拠要件のない存在型を展開するときに、またいくつかの使用例が散在している可能性がある。
![Ueeek](https://res.cloudinary.com/zenn/image/fetch/s--NP7Uoaam--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/c0a862dd95.jpeg)
Order of evaluation restrictions
存在型のboxを展開するには、boxを生成しその中身のunderlying typeを取り出す式を評価する必要がある。
その式の評価は副作用を持つ可能性がある。
たとえば、existential box 型のany P
を生成するgetP()
extension Int: P { }
func getP() -> any P {
print("getP()")
return 17
}
存在型の引数を展開してほしい、Generic関数を考える
func acceptFunctionStringAndValue<T: P>(body: (T) -> Void, string: String, value: T) { ... }
func hello() -> String {
print("hello()")
}
func implicitOpeningArgumentsBackwards() {
acceptFunctionStringAndValue(body: takeP, string: hello(), value: getP()) // will be an error, see later
}
value
パラメータへの引数を展開するには、getP()
を実行する必要がある。これは、body
パラメータの引数が実行されるまえに起こる必要がある。なぜなら、takeP
のGeneric型パラメータU
は そのExistential boxのunderlying型によってバインドされるからである。
それは、プログラムが下記の順番で副作用を発生させることを意味する
getP()
hello()
しかし、これはSwiftのleft->right
evaluation orderに矛盾する。
そうするより、ここれは、存在型の暗黙の展開に追加の制約を加えることにした。
- Generic型の引数がそのunderlying typeにバインドされて、そのunderlying typeは存在型の引数に対応するパラメータのあとに続く関数パラメータでも使用されている場合、存在型の引数は展開されない。
上記のimplicitOpeningArgumentsBackwards
では、acceptFunctionStringAndValue
はvalue
パラメータが展開されることを許可しない。なぜなら、そのGneric型のパラメータT
はbody
からも使用されているからである。
これは、underlying typeが展開された存在型引数の前にある引数には必要ないということを保証する。これによって、left->right
evaluationが維持される。
![Ueeek](https://res.cloudinary.com/zenn/image/fetch/s--NP7Uoaam--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/c0a862dd95.jpeg)
Avoid opening when the existential type satisfies requirements (in Swift 5)
これまで議論したように、存在型の展開は存在型のboxをGeneric関数に渡している既存のプログラムのコードの挙動を変える可能性がある。
例えば、パラメータをreturned arrayにいれている存在型boxを制約のないGeneric関数に渡すことによる影響を考えよう。
func acceptsBox<T>(_ value: T) -> Any { [value] }
func passBox(p: any P) {
let result = acceptsBox(p) // currently infers 'T' to be 'any P', returns [any P]
// unrestricted existential opening would infer 'T' to be the underlying type of 'p', returns [T]
}
ここで、acceptsBox
の返り値の動的な型は、存在型boxが呼び出しのときに展開されると、変化する。
変化自体は微々たるものだが、実行時まで見つけることができない。
これは、Generic Parameterのバインドに依存している既存のプログラムで問題を生じる可能性がある。
これまで動作していた存在型を伴うGeneric 関数の呼び出しはこれまでと同じ挙動で動作しつづける。
これまで動作していなかった存在型を伴うGeneric関数の呼び出しは、展開が実行されるため、成功する可能性がある。
Genericパラメータの存在型へのバインドをしているほとんどの場合で、Generic パラメーエタの要件への準拠は行なわれていなう。(e.g. acceptsBox
のT
)
ほとのどのプロトロルで、対応する型を参照している存在型はそのプロトコルに準拠していない。(e,g, any Q
はQ
に準拠していない)
しかし、少数の例外がある
- 存在型
any Error
はError
プロトコルに準拠している。 - @objc プロトコル
Q
の存在型any Q
は,Q
がstatic
な要件を含んでない場合Q
に準拠している
たとえば、以下のエラーを受け取る処理を考える。
any Error
を渡すのは、存在型の展開がなくても成功する。
func takeError<E: Error>(_ error: E) { }
func passError(error: any Error) {
takeError(error) // okay without opening: 'E' binds to 'any Error' because 'any Error' conforms to 'Error'
}
この提案では、存在型が対応するGeneric Parameterの要件を満している場合に存在型の展開を実行しないことで、挙動を保存する。
Swiftは少しづつ存在型がプロトコルに準拠するように機構を準備していくべきである(e.g any Hashable
はHashable
に準拠する)
Swift6ではこの挙動は適応されない。上記のpassBox
はp
を展開してT
にバインドする。
![Ueeek](https://res.cloudinary.com/zenn/image/fetch/s--NP7Uoaam--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/c0a862dd95.jpeg)
as any P
/ as! any P
Suppressing explicit opening with もし存在型の変数の暗黙の展開を無効にしたいなら、存在型にForce Castを明示的に書くことで実現できる。
func f1<T: P>(_: T) { } // #1
func f1<T>(_: T) { } // #2
func test(p: any P) {
f1(p) // opens p and calls #1, which is more specific
f1(p as any P) // suppresses opening of 'p', calls #2 which is the only valid candidate
f1((p as any P)) // parentheses disable this suppression mechanism, so this opens p and calls #1
}
存在型の暗黙の展開は Generic関数が展開を実行しないと呼び出せないときに発生すると定義したとき、Swift5ではこの無効にする機能はあまり必要にはならないだろう。
Swift6では、暗黙の展開がより積極的に発生するため、Swift5の挙動を保存したいときに、それを使用するとよい。
余分にかっこで囲むことで そのはたらきを無効にできる。
たとえば、型消去により返り値の型から情報が失われたことを認識するためです。
これはas
の両方の意味が当てはまる場合に、曖昧さを解消するのにやくだつ
protocol P {
associatedtype A
}
protocol Q {
associatedtype B: P where B.A == Int
}
func getP<T: P>(_ p: T)
func getBFromQ<T: Q>(_ q: T) -> T.B { ... }
func eraseQAssoc(q: any Q) {
getP(getBFromQ(q)) // error, must specify "as any P" due to loss of constraint T.B.A == Int
getP(getBFromQ(q) as any P) // suppresses error above, but also suppresses opening, so it produces
// error: now "any P does not conform to P" and op
getP((getBFromQ(q) as any P)) // okay! original error message should suggest this
}
![Ueeek](https://res.cloudinary.com/zenn/image/fetch/s--NP7Uoaam--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/c0a862dd95.jpeg)
Source compatibility
この提案では 特にSwift5において、ソースコード互換性に対するほどんどの影響を回避するために特別に定義されている。
以前では無効だったGeneric functionの呼び出し(e,g "any P doesn not conform to P)は有効になる。
既存のコードはこれまでと同じように動作する。
以前は成功していたオーバーロードの解決は引き続き成功するが、別のオーバーロードが選択される可能性がある
protocol P { }
func overloaded1<T: P, U>(_: T, _: U) { } // A
func overloaded1<U>(_: Any, _: U) { } // B
func changeInResolution(p: any P) {
overloaded1(p, 1) // used to choose B, will choose A with this proposal
}
実際にはこれらの例が問題を引き起こすことはほとんどないだろう。
Effect on ABI stability
This proposal changes the type system but has no ABI impact whatsoever.
Effect on API resilience
This proposal changes the use of APIs, but not the APIs themselves, so it doesn't impact API resilience per se.
![Ueeek](https://res.cloudinary.com/zenn/image/fetch/s--NP7Uoaam--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/c0a862dd95.jpeg)
+(OK)コンパイルが通ってなかったコードが、コンパイルが通るようになる。
- 違うオーバーロードが選択されてしまう可能性がある。→ 影響となりうる。