Swift - プロトコルが型になるとき


@koher さんの記事、なぜSwiftのプロトコルはジェネリクスをサポートしないのかにおける議論は改めて記事にするだけの価値があるのでそうすることにします。

君の型は?

まずは以下のコードをご覧ください。

import Foundation

protocol Animal {
    var binomen:String { get }
    init()
}
struct Cat: Animal {
    let binomen = "Felis silvestris catus"
    let theYoung = "kitten"
}
struct Dog: Animal {
    let binomen = "Canis lupus familiaris"
    let theYoung = "puppy"
}

var pet:Animal = (arc4random() & 1 == 1) ? Cat() : Dog()
  • Q0: このコードは動きますか?
    • A0: 動く
    • A1: (type mismatchで動かない)

正解はA0、きちんと動きます。PlaygroundsなりREPLで確認してみてください。で、本題です。

  • Q1: petの型は?
    • A0: CatDogか動的に決まる
    • A1: 何らかの型に静的に決まる

正解はA1。そして型の名はCatでもDogでもないAnimalです。

Playgroundの型インスペクターを見てももろそう書いてありますし、.binomenにアクセスできても.theYoungにはアクセスできません。どちらにも.theYoungは存在していますがAnimalには知る由もないのです。

let 🐱 = Cat()
🐱.theYoung // "kitten"
let 🐶 = Dog()
🐶.theYoung // "puppy"

ならば

pet = Animal()

も動きそうなものですが、protocol type 'Animal' cannot be instantiatedと怒られてしまいます。あくまで具現化(instantiation)はstructenumclassで、protocolは本来の「規約」として使えということですね。

そのことがわかれば、Swiftの総称型(generics)の設計もわかります。

全ての定数(let)と変数(var)の型はコンパイル時に静的に決まる

ほんと、これだけです。なぜ

func ditto<T>(x:T)->T { return x }

がOKなのに


let ditto<T>:(T)->T = { $0 }

はNGなのか。まさにこれが理由です。Tに何が入るのかは、コンパイル時に決まってなければならないから。実行時に決まる言語であれば

// ECMAScript
const ditto = (x)=>x

と書けるのとそこが大いに異なります。Type<T>がOKでProtocol<T>がNGなのもそれ。その悩みを解決したのがassociatedtypeで、Protocolでは

protocol Sequence {
  associatedtype Element
}

としてElementという型が内包されていることを強制した上で、

struct Array<Element> : Sequence {
  //...
}

とすることで確実に型が静的に決まるようにしているわけです。さらに今では

// Element が Numeric の場合のみ.sum()できるように
extension Array where Element:Numeric {
    func sum()->Element {
        return self.reduce(0, +)
    }
}

というように拡張の範囲を限定できたりもします。

まとめ

というわけでまとめます。

このあたりの感覚は、実際に自分で型を作るとよくわかります。楽しいぞい。

Dan the Safe, Fast and Expressive

追記:Anyの正体

コメントの議論にもありますが、これはやはり特記しておくべきですね。実は「何でも入る」型であるAnyもプロトコル型です。実際明示的にAnyをプロトコルとして使うことも可能ですし…

struct Whatever:Any {} // no error

REPLでAny.selfを覗いてみて見るとはっきりとそれがわかります。

1> Any.self
$R0: Any.Protocol = Any

つまり

protocol Any {}

で、全ての型宣言の裏では…

struct S {/*…*/}
enum E {/*…*/}
class C {/*…*/}

暗黙的にAnyプロトコルに準拠している…

struct S:Any {/*…*/}
enum E:Any {/*…*/}
class C:Any {/*…*/}

というわけなのです。