Heart of Swift を読んだ
前から気になっていた Heart of Swift を読んだ。とても面白かったのでメモしておく。
Heart of Swift は、値型が中心になっているという特徴を持つ Swift でどのように抽象化を行っていくべきかが書かれている文章だ。
まず第1章の「Value Semantics」でそもそも値型とは何でどういうところが良いのかや、なぜ Swift が値型を中心にできたのかということが説明される。第2章の「Protocol-oriented Programming」では第1章の内容を前提にして、
- Swift では参照型が中心の言語とは異なる抽象化の方法をとる必要があること
- protocol には型としての利用と制約としての利用の2種類があること
- リバースジェネリクスの考え方やOpaque Result Type が解決する問題
などについて書かれている。
二部構成になっているのが良くて、自分がとくに面白いなと思ったのは2章の protocol とか Opaque Result Type の話で、そこは Value Semantics の話がなくても面白く読めそうなんだけど、1章があることによってそれらの抽象化の方法は値型が中心の言語であるからこそ必要なんだと納得できて Swift の思想が理解できた気になり満足感がある。
以下とくに勉強になったことのメモ。
型として・制約としての protocol
protocol は型として使われる場合と制約として使われる場合がある。わかりやすい例は以下。
protocol Animal { func foo() -> Int } struct Cat: Animal { func foo() -> Int { 1 } } struct Dog: Animal { func foo() -> Int { 2 } } // Animal は型として使われている func useAnimal(_ animal: Animal) { print(animal.foo()) } // Animal は制約として使われている(ジェネリクス) func useAnimal<A: Animal>(_ animal: A) { print(animal.foo()) }
制約としての protocol はジェネリクスと一緒に使われる。もちろん型としての protocol でしか実現できないことはあるが、どちらでもよい場合は protocol は制約として用いるべきだと書かれている。これは、 protocol を型として使う場合は実行時にどんな型が入ってくるのかわからないため、どんな型でも入る Existential Container というものに包む必要があるからだ。これにより実行時に Existential Container の分のメモリを使ってしまうことや、Existential Container に包んだり剥がしたりするオーバーヘッドがかかることが問題になる。
var dog: Dog = Dog() var animal: Animal = Dog() print(MemoryLayout.size(ofValue: dog)) // 0 print(MemoryLayout.size(ofValue: animal)) // 40。Existential Container のサイズ
この理由で、protocol は型ではなく制約として使うべき。また、(これは本質的な問題はないのでそのうち解決するかもと書かれているが)assosiatedtype をもつ protocol は型として使うことができないのでそもそも制約として使うしかない。
リバースジェネリクスと Opaque Result Type
protocol を引数として使う場合は制約として使うのが良いことはわかった。返り値に protocol を使う場合についても同じように制約として使いたいが、今の Swift では完全にそれを実現することはできない。ジェネリクスの逆のリバースジェネリクスという仕様が Swift に入ってないからだ。
// Animal は型として使われている。パフォーマンスの観点から望ましくない func makeAnimal() -> Animal { Cat() } // Animal は制約として使われている(リバースジェネリクス)。現状の Swift の仕様にはない func makeAnimal<A: Animal>() -> A { Cat() // コンパイルエラー }
しかし、実はリバースジェネリクスで実現したいことは部分的には Swift 5.1 で入った Opaque Result Type で実現できるようになっている。SwiftUI でよく使うやつ。
struct MyView: View { var body: some View { // Opaque Result Type // ... } }
上の例では、 body
は View
protocol に従う何らかの具体的な型であるということを言っている。つまり、 View
は制約として使われている!
比較のために View
protocol を型として使うことを考えてみると var body: View {}
という定義になりそうだが、このようにしてしまうと開発者が body
は View
に従っているということだけ知っておけばよくて具体的な型を意識しなくてよいという点で protocol のメリットを享受できる一方でパフォーマンスの問題がある。もちろん、実質的な問題として現状 assosiatedtype をもつ View
は型として使えないので var body: View {}
の表記はコンパイルエラーになるという話もある。
Opaque Result Type の var body: some View{}
では開発者は body
の具体的な型を意識しなくてよいというメリットは残しつつコンパイラは具体的な型を知っているので実行時のオーバヘッドがない。