今更ながら『Heart of Swift』を読んだので、勉強用としてまとめる。
対外的にまとめるものではないです。
第1章 Value Semantics - 【勉強用】Heart of Swift - 1
第2章 Protocol-oriented Programming - この記事
はじめに
Swiftは値型を中心としたプログラミング言語である。
Heart of Swift では Swift の中心となる概念を通して Swift という言語のコンセプトが書かれている。
中心となる概念は以下2つ。
- Value Semantics
- Protocol-oriented Programming
これらについては WWDC2015 のセッションの中で詳しく説明されている。
Protocol-oriented Programming とは
WWDC2015のセッション“Protocol-Oriented Programming in Swift”では、
- Self-requirement
- Protocol Extension
- プロトコルの後付け
など数多くの例が取り上げられているが、何が Protocol-oriented Programming なのかは明確に述べられていない。
筆者(koherさん)の解釈として、Protocol-oriented Programmingという用語は、Object-oriented Programming(オブジェクト指向プログラミング)との対比によって生み出されたものだと考える。
Swiftにおけるプロトコルは抽象化の道具。
オブジェクト指向プログラミングでは、
- クラスの継承
- ポリモーフィズム
を用いてコードを抽象化する。
しかしSwiftは値型中心の言語であり、原理的に継承することができない。
そのためSwiftではクラスの継承ではなくプロトコルが抽象化の主役となる。
Protocol-oriented Programmingとは、そのようなプロトコルを用いてコードを抽象化する手法全般を指している考える。
プロトコルによる不適切な抽象化
以下のようなコードはSwiftにおいて必ずしも適切なプロトコルの使い方ではない。
protocol Animal {
func foo() -> Int
}
struct Cat: Animal {
func foo() -> Int { 2 }
}
struct Dog: Animal {
func foo() -> Int { 1 }
}
let animal: Animal = Bool.random() ? Cat() : Dog()
print(animal.foo())
Existential TypeとExistential Container
プロトコル型変数にインスタンスを格納する際には、Existential Containerという任意のサイズのインスタンスを格納できる入れ物が用いられる。
インスタンスはExistential Containerに入れられた上で変数に格納される。
protocol Animal {
func foo() -> Int
}
struct Cat: Animal {
var value: UInt8 = 2 // 1バイト
func foo() -> Int { 2 }
}
struct Dog: Animal {
var value: Int32 = 1 // 4バイト
func foo() -> Int { 1 }
}
// 値型のためインスタンスが直接変数に格納される
// それぞれの変数は以下のバイト数の領域を必要とする
let cat: Cat = .init() // 1バイト
let dog: Dog = .init() // 4バイト
let animal: Animal = Bool.random() ? cat : dog
print(MemoryLayout.size(ofValue: animal) // 40バイト
Animal
型変数に格納できるのはCat
とDog
のインスタンスだけではない。
Animal
型として考えると適合した任意の型のインスタンスを格納できなくてはならず、Animal
に適合した型は理論上いくらでも大きくできる(1バイトのStored Propertyを1000個持たせれば1000バイトになる)。
Existential Containerについての詳細は以下
Swiftリポジトリにあるドキュメント"Type Layout"の"Existential Container Layout"を参照。
参照型とポリモーフィズム
Existential Containerのオーバーヘッドは値型とプロトコルに起因するものである。
全てクラスにした場合、参照型はインスタンスが存在するメモリのアドレスが変数に格納されるため8バイトで表される。
そのため代入する際Existential Containerに包む必要はない。
class Animal {}
class Cat: Animal {}
class Dog: Animal {}
let cat: Cat = .init() // 8バイト
let dog: Dog = .init() // 8バイト
let animal: Animal = Bool.random() ? cat : dog // 8バイト
値型に適したポリモーフィズム
サブタイプポリモーフィズム
プロトコルによってサブタイプポリモーフィズムを実現する場合、プロトコルは型として用いられる。
以下のコードではAnimal
プロトコルがAnimal
型として用いられる。
func useAnimal(_ animal: Animal) {
print(animal.foo())
}
サブタイムポリモーフィズムの場合、useAnimal
に引数を渡すときにインスタンスをExistential Containerに包む必要がある。また、animal.foo()
を呼び出す箇所ではanimal
がCat
かDog
か、また別のインスタンスかは実行時までわからない。
useAnimal(Cat())
useAnimal(Dog())
これら2つはどちらも同じように使うことができ、実行結果も同じ。
しかし、Cat
のfoo
を呼び出すか、Dog
のfoo
を呼び出すかは実行時に決定される(動的ディスパッチ)
パラメトリックポリモーフィズム
以下のコードではAnimal
プロトコルが型パラメータA
の制約として用いられる。
ジェネリック関数は様々な型に対して個別に関数を実装する代わりに、まとめて1つの関数として実装する手段と言える。
挙動としてはサブタイプポリモーフィズムとして実装した時と同じ。
func useAnimal<A: Animal>(_ animal: A) {
print(animal.foo())
}
Cat
やDog
などのそれぞれの型に対して関数をオーバーロードする代わりに、型パラメータという概念を導入して1つの関数として実装できるようにしたのがジェネリック関数。
実行時の挙動は、コンパイラが特殊化(最適化の一種)を行った場合、ジェネリック関数としてのuseAnimal
のバイナリに加えて、型パラメータにCat
やDog
などの具体的な方を当てはめたuseAnimal
のバイナリも生成される。
そしてuseAnimal
にCat
インスタンスを渡す箇所ではCat
用の、Dog
インスタンスを渡す箇所ではDog
用のuseAnimal
が呼び出されるようコンパイルされる。
そのためExistential Containerに包む必要がなく、インスタンスが直接それぞれのuseAnimal
関数に渡される。
animal.foo()
を呼び出す箇所についても、Cat
用のuseAnimal
の中ではanimal
がCat
であるとわかっているため、コンパイル時にCat
のfoo
を呼び出せばいいと決定できる(静的ディスパッチ)。
そのため実行時にメソッドを選択するオーバーヘッドが発生しない。
値型中心のSwiftにおいては、値型と組み合わせた時にオーバーヘッドの大きいサブタイプポリモーフィズムよりも、オーバーヘッドの発生しないパラメトリックポリモーフィズムの方が適している。
制約としてのプロトコル
- プロトコルを「型として」ではなく「制約として」使用することを優先する
“Protocol-Oriented Programming in Swift”の中では特別そのことを強調されていないが、標準ライブラリにおけるプロトコルの利用例を見る限り、これこそがSwiftのプロトコルの使い方について最も重要なことだと考えられる。
「型として」・「制約として」のプロトコルの使い分け
// 型としてのプロトコル
func useAnimal(_ animal: Animal) {
print(animal.foo())
}
// 制約としてのプロトコル
func useAnimal<A: Animal>(_ animal: A) {
print(animal.foo())
}
型としてのプロトコルでしかできないこと
Heterogeneous Collection(異なる型のインスタンスが混在したコレクション)が必要な場合はプロトコルを型として使う必要がある。
制約としてのプロトコルの場合はHeterogeneousでないArray
しか渡すことしかできない。
// 型としてのプロトコル
func useAnimals(_ animals: [Animal]) {
...
}
useAnimals([Cat(), Dog()])
// 制約としてのプロトコル
func useAnimals<A: Animal>(_ animals: [A]) {
...
}
useAnimals([Cat(), Dog()]) // コンパイルエラー
useAnimals([Cat(), Cat()]) // [Cat]を渡す(`A`はCat)
useAnimals([Dog(), Dog()]) // [Dog]を渡す(`A`はDog)
型パラメータA
にAnimal
を当てはめられない理由
プロトコル型がそのプロトコル自体に適合することをSelf-conformanceというが、一般的なSwiftのプロトコルはSelf-conformanceを持たない。
例として、Animal
型はAnimal
プロトコル自体に適合しないため、useAnimal
にAnimal
型の値を渡すことはできない。
func useAnimal<A: Animal>(_ animal: Animal) { ... }
let animal: Animal = Cat()
useAnimal(animal) // コンパイルエラー
ただし、例外的にSE-0235でError
プロトコルにSelf-conrmancceが追加された。
ref: https://github.com/apple/swift-evolution/blob/master/proposals/0235-add-result.md#adding-swifterror-self-conformance
制約としてのプロトコルでしかできないこと
代表的な例はSelf-requirement。Equatable
プロトコルではSelf-requirementが使われている。
Self-requirementを持つプロトコルは制約として使うことだけが想定されている。
型として使うことに意味がないため、コンパイラが型として使うこと自体を禁止している。
protocol Equatable {
static func == (
lhs: Self, // Self-requirement
rhs: Self // Self-requirement
) -> Bool
}
extension Int: Equatable {
static func == (
lhs: Int, // SelfがIntに置き換えられる
rhs: Int // SelfがIntに置き換えられる
) -> Bool { ... }
}
extension String: Equatable {
static func == (
lhs: String, // SelfがStringに置き換えられる
rhs: String // SelfがStringに置き換えられる
) -> Bool { ... }
}
// 制約としてのプロトコル
extension Sequence where Element: Equatable { // Equatable が制約として使われている
func contains(_ element: Element) -> Bool {
...
}
}
// 型としてのプロトコル
let a: Equatable = 42
let b: Equatable = "42"
a == b // Int と String の比較はどこにも実装されていないためコンパイルエラー
let intA: Equatable = 42
let intB: Equatable = 42
intA == intB // Int 同士でも Equatable を介すとコンパイルエラー
let anyA: Any = 42
let anyB: Any = 42
anyA == anyB // Any 同士に == は実装されていないためコンパイルエラー
プロトコルとリバースジェネリクス
様々なユースケースが生じる中で、制約としてのプロトコルによるコードの抽象化に欠けているものが明らかになってきた。
そのような問題と解決策についてまとめられたドキュメントが “Improving the UI of generics” 。
このドキュメントの中で、リバースジェネリクスという新しい概念が説明され、その簡易系であるOpaque Result TypeがSwift5.1で部分的に導入された。
制約としてのプロトコルに欠けていた抽象化
Swiftの標準ライブラリでも制約としてのプロトコルが広く使われており、ほとんどすべてのプロトコルが制約として使用されている。
しかし、メソッドの戻り値の具体的な型を知る必要があるという問題がある。またもう一方で抽象的に書く場合Existential Containerのオーバーヘッドを受け入れる必要があるという問題もある。
protocol Sequence {
associatedtype Iterator: IteratorProtocol // 制約として使われている
func makeIterator() -> Iterator
}
extension Array: Sequence {
// IteratorProtocl が IndexingIterator<[Element]>という具体的な型に置き換わる
func makeIterator() -> IndexingIterator<[Element]> { ... }
}
extension String: Sequence {
// IteratorProtocl が String.Iteratorという具体的な型に置き換わる
func makeIterator() -> String.Iterator { ... }
}
let array = [1, 2, 3]
// iteratorの型はIndexingIterator<[Int]>
var iterator = array.makeIterator()
上記コードのようにIndexingIterator<[Int]>
が公開されているため、IteratorProtocol
に適合する型を実装する際は、Swift標準ライブラリのIteratorProtocolに適合する大量のイテレータ型を意識する必要がある。
これ自体は問題ではないが、利用時にIndexingIterator<[Int]>の型を意識する必要ないのだが、型が公開されているため将来的にArrayに特化したより高速のイテレータが実装されとしてもmakeIteratorの戻り値の型を変更するのが困難になっている。
仮に以下のようにArrayのmakeIteratorが抽象化されていたら、Arrayに特化したより高速なイテレータに差し替えられてもAPIとしての表面上の型に変更はない。
extension Array: Sequence {
func makeIterator() -> Elementを取り出す何らかのイテレータ { ... }
}
利用者にとって本来必要なのはこのレベルの抽象度であり、具体的なイテレータの型を知る必要はない。
しかしかといって戻り値の型にGeneralized Existentialを使う(IteratorProtocolを型として使う)とExistential Containerのオーバーヘッドの問題が残ってしまう。
※今のSwiftではGeneralized ExistentialとSelf-conformanceがサポートされていないため不可能
今望まれているのは、抽象的にコードを書きながら、具象型と同じパフォーマンスを出せること
抽象的なコードと具象型のパフォーマンス(戻り値の場合)
以下のような引数の型を抽象化する関数と、戻り値の型を抽象化する場合、利用者と実装者の関係が逆転する。
useAnimal: 利用者が具象型を決定し、実装者が抽象型を使用する。
makeAnimal: 実装者が具象型を決定し、利用者が抽象型を使用する。
func useAnimal<A: Animal>(_ animal: A) {
print(animal.foo()) // 実装者が A を使用
}
useAnimal(Cat()) // 利用者が A を Cat に決定
func makeAnimal() -> A {
Cat() // 実装者が A を Cat に決定
}
let animal = makeAnimal() // 利用者が A を使用
通常では戻り値の型を抽象化できないが、可能にするものとしてリバースジェネリクスという概念が提唱された。
ref: https://forums.swift.org/t/reverse-generics-and-opaque-result-types/21608
doc: “Improving the UI of generics”
Swift5.1時点ではリバースジェネリクスは採択されていないが、サブセットと言えるOpaque Result TypeはSwift5.1で部分的にサポートされた。
仮に採択された場合は以下のように書くことができようになる。
func makeAnimal() -> <A: Animal> A {
Cat()
}
let animal = makeAnimal()
print(animal.foo())
let cat: Cat = animal // コンパイル時には A と Cat は明確に区別されるためコンパイルエラー
extension Array: Sequence {
func makeIterator() -> <I: IteratorProtocol> I where I.Element == Element { ... }
}
// makeIterator メソッドにリバースジェネリクスが使われたとき
var iterator: IndexingIterator<[Int]> = [2, 3, 5].makeIterator() // コンパイルエラー
// 以下のような書き方を強制する
var iterator = [2, 3, 5].makeIterator()
Opaque Result Type
Opaque Result Typeはリバースジェネリクスを簡潔に書くためのシンタックスシュガーだと考えることができる。
SE-0244で部分的に採択され、Swift5.1でサポートされた
ref: https://github.com/apple/swift-evolution/blob/master/proposals/0244-opaque-result-types.md
以下のコードは全く同じことを意味する。
// リバースジェネリクス
func makeAnimal() -> <A: Animal> A {
Cat()
}
// Opaque Result Type
func makeAnimal() -> some Animal {
Cat()
}
Opaque Argument Type
Opaque Result Typeと同じように、ジェネリックな引数をsomeを使って簡潔に書けるようにしようというものがOpaque Argument Type。
以下のコードは全く同じ意味になる。
// ジェネリクス
func useAnimal<A: Animal>(_ animal: A) {
print(animal.foo())
}
// Opaque Argument Type
func useAnimal(_ animal: some Animal) {
print(animal.foo())
}
ジェネリクスでしかできないこと
Opaque Result TypeとOpaque Argument Typeを合わせてOpaque Typeと呼ぶ。
Opaque Typeがあればジェネリクスが不要になる、ということはなくジェネリクスにしかできないことがある。
同種のAnimal
を2つ引数に取りたい場合にはジェネリクスを使うしかないため、Opaque Typeはジェネリクスでできることの一部を簡潔に書くための手段でしかない。
// ジェネリクス
func useAnimalPair<A: Animal>(_ pair: (A, A)) {
...
}
// Opaque Type
func useAnimalPair(_ pair: (some Animal, some Animal)) {
...
}
// ↓と同じ意味になってしまう
func useAnimalPair<A1: Animal, A2: Animal>(_ pair: (A1, A2)) { // これはダメ
...
}
Opaque TypeとExistential Type
Opaque Typeにsome
というキーワードが必要なように、Existential Typeにもany
というキーワードを付与する。
func useAnimal(_ animal: any Animal) { // any があるので「型として」
print(animal.foo())
}
func useAnimal<A: Animal>(_ animal: A) { // any がないので「制約として」
print(animal.foo())
}
また、any
というキーワードはsome
と対比した時に複数のAnimal
を考えると、
[some Animal]
: ある(some
) Animal
のArray
[any Animal]
: 任意の(any
) Animal
のArray
となり、言語的に自然である。
func useAnyAnimals(_ animals: [any Animal]) {
animals.forEach { print($0.foo()) }
}
useAnyAnimals([Cat(), Dog()])
useAnyAnimals([Cat(), Cat()])
func useSomeAnimals(_ animals: [some Animal]) {
animals.forEach { print($0.foo()) }
}
useSomeAnimals([Cat(), Dog()]) // コンパイルエラー
useSomeAnimals([Cat(), Cat()])
プロトコルとリバースジェネリクスのまとめ
引数 | 戻り値 | |
---|---|---|
ジェネリクス 制約としてのプロトコル |
<A: Animal>(A) -> Void |
() -> <A: Animal> A |
Opaque Type ジェネリクスのシュガー |
(some Animal) -> Void |
() -> some Animal |
Existential Type 型としてのプロトコル |
(any Animal) -> Void |
() -> any Animal |
ジェネリクスとOpaque Typeはコンパイル時に静的に抽象化を行う。コード上で抽象化された型がコンパイル時に特殊化によって展開され、実行時のパフォーマンスに影響がない抽象化が可能になる。
Existential Typeは実行時に動的に抽象化を扱う。この時の主役は値で、値はプロトコルに適合していることだけが求められ、その型は重要視されない。値の挙動はExistential Containerを用いて動的に解決される。