この記事ではSwiftのProtocol ExtensionがExistentialをopenする事を説明します。
Existentialと型変数の違い
SwiftのExistentialは、プロトコルに対して自動定義される型で、そのプロトコルを満たす型を代入できます。
protocol P {}
extension Int : P {}
var p: P = 3
ここでp
の型はP
のexistentialです。P
のexistentialに代入できるのは、P
を満たす型なので、つまり、ジェネリクスの<T: P> T
で表される型変数です。Int
は<T: P> T
にマッチしますね。
ここで、P
のexistentialと<T: P> T
の違いを説明します。
P
のexistentialは、条件を満たす型がなんでも代入できる型です。この型になった時点で、元の値の型の情報は消去されています。そのため、例えば下記のように異なる型を再代入する事ができます。
protocol P {}
extension Int : P {}
extension String : P {}
var p: P = 3
p = "str"
一方、<T: P> T
は、条件を満たす特定のある型を表しています。実行時の実際の値の型が
何になっているかはわかりませんが、コンパイル時の型検査ではT
という表現で特定の型を表しています。そのため、下記のように特定の型を代入する事はできません。
func g<T: P>(_ x: T) {
var x = x
x = "" // Cannot assign value of type 'String' to type 'T'
}
このように、existentialと型変数は、実行時に代入できる型の条件は同じでも、コンパイル時の型検査においては、型の同一性が失われている、維持しているのかという違いがあります。
SwiftのCore TeamメンバーのJoe Groffは、この違いをvalue-level abstraction
(Existential)とtype-level abstraction
(型変数)と説明しています。
Existentialのopen
ここで、Existentialから型変数を取り出す事を考えることができます。つまり、コードにおいて、Existentialに入っている型の同一性を復旧させます。
例1
この概念は、Generics Manifestoで紹介されています。
let e1: Equatable = ...
let e2: Equatable = ...
if let storedInE1 = e1 openas T { // T is the type of storedInE1, a copy of the value stored in e1
if let storedInE2 = e2 as? T { // Does e2 have type T? If so, copy its value to storedInE2
if storedInE1 == storedInE2 { ... } // Okay: storedInT1 and storedInE2 are both of type T, which we know is Equatable
}
}
この例は、Equatable
のexistentialであるe1
とe2
を==
で比較する架空のコードです。実際の現在のSwiftにおいては、Equatable
のexistentialはありませんが、それが仮にあった場合の話です。
このコードでは、openas
という架空の言語機能を使って、e1
から真の型T
を取り出し、e2
を、そのT
にキャストした後で、e1
とe2
の==
比較をしています。この、真の型を取り出す操作をExistentialのopenと言います。
なぜこのような手間を踏む必要があるかというと、Equatable
プロトコルは自身と同じ型を比較するためのプロトコルだからです。その機能を提供する==
メソッドの型はfunc ==(lhs: Self, rhs: Self) -> Bool
であり、両辺が同じ型でなければ呼び出せません。Equatable
のexistentialであるe1
とe2
にはそれぞれ異なる型が入っているかもしれないので、そのままでは比較できないのです。
ちなみに、本題とは関係ありませんが、標準ライブラリにはAnyHashable
というtype erasureが提供されていて、これを使うと異なる型の等値比較を行うことができます。
var e1 = AnyHashable(Int(3))
var e2 = AnyHashable(Double(3.0))
print(e1 == e2) // true
例2
Joe Groffがフォーラムに投稿した書き込みにもう一つの例があります。
func bar(x: Collection) {
let <X: Collection> openedX = x // X is now bound to the dynamic type of x
let start = openedX.startIndex
let first = openedX[start] // OK, indexing X with a value of type X.Index, to get a result of type X.Element
}
この例では、また別の架空の構文を使って、Collection
のexistentialであるx
から、真の型をX
として取り出しています。これにより、openedX
のsubscriptにopenedX
のstartIndex
を渡しています。
なぜこのような処理が必要かというと、Collection
は自身のIndex
型を持っているが、existentialのままではその型情報が消えてしまうために、型安全なコードが書けないからです。
例えば標準ライブラリにはAnyCollection
というtype erasureがありますが、これを使うと下記のようなコードを書くことができます。このコードはコンパイルすることはできますが、実行時にクラッシュしてしまいます。それは、真の型Array<Int>
のsubscriptに対して、String.Index
を渡しているからです。
var c1 = AnyCollection([3])
var c2 = AnyCollection("str")
print(c1[c2.startIndex])
仮にCollection
のexistentialがあったとしても、これと同様に、真の型がわからないので、真の型に対応したIndex
型もわからず、型安全にできないのです。
Protocol Extensionによるopen
ここまで見たように、Existentialのopenという概念は公式に説明されていますが、実際の言語仕様としては導入されていません。しかし実は、Protocol Extensionにおいては、すでにExistentialのopenが実現されているのです。
protocol P {
func f()
}
extension Int : P {}
extension P {
func f() {
}
}
このように、プロトコルP
のメソッドf
が、Protocol Extensionによって実装を与えられている状況を考えます。
このとき、P
のexistential型に対しても、f
を呼び出すことができます。
var x: P = 3
x.f()
実はここでExistentialがopenされています。これは、P.f
におけるself
の型を考える事でわかります。
P.f
の型は何になっているのでしょうか。もし、P
のexistentialであったとしたら、前述の例と同様にして、self
の型の変数に対して、何かP
を満たす型を代入できるはずです。
extension P {
func f() {
var x = self
x = Int(3) // Cannot assign value of type 'Int' to type 'Self'
}
}
しかし、代入することはできません。一方で、self
はP
を満たしているとして扱えます。下記のように他にもメソッドg
があったとき、それを呼び出すことができます。
protocol P {
func f()
func g()
}
extension P {
func f() {
g()
}
}
self
はこのような特性をもっているので、<T: P> T
という型になっていると考えられます。
もう一つの根拠として、このコードのSILコードがあります。
// P.f()
sil hidden @$s1a1PPAAE1fyyF : $@convention(method) <Self where Self : P> (@in_guaranteed Self) -> ()
P.f
はこのように、<Self where Self : P>
というジェネリックシグネチャを持っていて、引数の型がSelf
になっています。
そして、このextensionによって自動実装されたInt.f
のSILは下記のようになります。
// protocol witness for P.f() in conformance Int
sil private [transparent] [thunk] @$sSi1a1PA2aBP1fyyFTW : $@convention(witness_method: P) (@in_guaranteed Int) -> () {
// %0 // user: %2
bb0(%0 : $*Int):
// function_ref P.f()
%1 = function_ref @$s1a1PPAAE1fyyF : $@convention(method) <τ_0_0 where τ_0_0 : P> (@in_guaranteed τ_0_0) -> () // user: %2
%2 = apply %1<Int>(%0) : $@convention(method) <τ_0_0 where τ_0_0 : P> (@in_guaranteed τ_0_0) -> ()
%3 = tuple () // user: %4
return %3 : $() // id: %4
} // end sil function '$sSi1a1PA2aBP1fyyFTW'
これを見ると、P.f
である$s1a1PPAAE1fyyF
に対して、型変数<Self>
にInt
を与えて呼び出している事が確認できます。これは通常のジェネリックメソッドと同じ機構になっています。
このように、self
の型が<Self : P> Self
という型変数になっているのは、実はextensionに限らずプロトコルのメソッドの共通の特徴です。
Self conformanceとopen
protocolのexistentialは自分自身のprotocolに準拠しません。詳細は以前書いた記事を参照してください。
これは記述でいうと、P
は<T: P> T
にマッチしないということです。一方で、ここまでで見たように、protocol extensionのメソッドが呼び出される時、P
型であるself
が<Self: P> Self
型としてopenされています。この違いを説明します。
まず、なぜexistentialをopenできるのかというと、P
型の変数には、実際にP
を満たす真の型を持った値が入っているからです。<Self: P> Self
としてopenされているのは、この真の値が対象なのです。この値の型がP
を満たしているという事は、P
の存在型に値を代入する際の条件でした。よって、それがopenできるのは当然です。
一方、protocolのself conformanceとして議論しているのは、P
のexistential型が、プロトコルP
に準拠できるか、ということです。これは特定の値とは関係の無い、型の話です。
ここで逆に、P
型がself-conformanceを満たす条件を考えてみます。それは@objc
がついていて、スタティックメンバを持たないことです。スタティックメンバを持たないということは、そのP
型に対して何か関わるときは、かならずインスタンスが絡んでいるということです。そして、インスタンスが絡んでいるということは、内部にself
を保持したexistential型の値が対象になっているということです。これはexistentialがopenできる理由と同じ事を考えているとわかります。
protocol extensionによるself conformanceの制約の回避
これまでの話で、protocol extensionを使うとexistentialがopenしていることがわかりました。これを利用すると、self-conformanceがなくて困った場合に、問題を回避できる場合があります。
例えば、下記のように、P
型がEncodable
であるときに、P
のexistentialを持つ型S
があったとします。
protocol P : Encodable {}
extension Int : P {}
struct S {
var p: P
init() { p = 3 }
}
S
の値があるとき、p
をencode
しようと思っても、JSONEncoder
は<X: Encodable> X
は受け取れますが、Encodable
のexistentialはサポートしていないため、できません。
func main(_ s: S) throws {
let encoder = JSONEncoder()
try encoder.encode(s.p) // Protocol type 'P' cannot conform to 'Encodable' because only concrete types can conform to protocols
}
そこで、P
に下記のようなextensionを与えて、それ経由の呼び出しに書き換えると、encodeができます。
extension P {
func encode(jsonEncoder: JSONEncoder) throws -> Data {
return try jsonEncoder.encode(self)
}
}
func main(_ s: S) throws {
let encoder = JSONEncoder()
try s.p.encode(jsonEncoder: encoder)
}
合わせて読みたい
型システムの理論からみるSwiftの存在型(Existential Type)
Swiftでprotocol型の値がそのprotocol自身にconformしていない理由