LoginSignup
38
21

More than 3 years have passed since last update.

SwiftのProtocol ExtensionはExistentialをopenする

Posted at

この記事では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であるe1e2==で比較する架空のコードです。実際の現在のSwiftにおいては、Equatableのexistentialはありませんが、それが仮にあった場合の話です。

このコードでは、openasという架空の言語機能を使って、e1から真の型Tを取り出し、e2を、そのTにキャストした後で、e1e2==比較をしています。この、真の型を取り出す操作をExistentialのopenと言います。

なぜこのような手間を踏む必要があるかというと、Equatableプロトコルは自身と同じ型を比較するためのプロトコルだからです。その機能を提供する==メソッドの型はfunc ==(lhs: Self, rhs: Self) -> Boolであり、両辺が同じ型でなければ呼び出せません。Equatableのexistentialであるe1e2にはそれぞれ異なる型が入っているかもしれないので、そのままでは比較できないのです。

ちなみに、本題とは関係ありませんが、標準ライブラリには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にopenedXstartIndexを渡しています。

なぜこのような処理が必要かというと、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'
    }
}

しかし、代入することはできません。一方で、selfPを満たしているとして扱えます。下記のように他にもメソッド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の値があるとき、pencodeしようと思っても、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していない理由

38
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
38
21