これはiOSDC Japan2019の発表時に調べた資料の一部です。
Swiftでコードを書く上で
Protocolに出会わないことはないと言っても過言ではないと思います。
Protocolには
- ジェネリクスな型に制約を与えてコンパイル時の型チェックやパフォーマンスの最適化を行う静的な面
- 直接型として使用して実行時に具体的な型として定義したプロパティやメソッドを利用できるようにする動的な面(ダイナミックディスパッチ)
の2つの顔があり
使い方は多岐に渡ります。
今回は
そんなProtocolはそもそもどういう存在で
それを利用する意義とはなのかということに関して
Swift Forumの議論を通して学んでみたいと思います。
今回は↓の記事の意訳です。
https://oleb.net/blog/2016/12/protocols-have-semantics/
本文ここから
今週swift-evolution(Swift Forumの前にSwiftプロポーザルを議論していた場所)に
とても興味深い(そして長い)ディスカッションがされています。
ある人がDefaultConstructible
という引数を持たないinitのみを必須とするProtocolを
Standard Libraryに追加するかどうかの話し合う場を設けました。
protocol DefaultConstructible {
init()
}
言い換えるならば、
このProtocolはある種の「デフォルト値」や
追加情報なしにProtocolに適合している型のインスタンスを作成することを
正式に認めることになります。
何人かの人、主にXiaodi WuとDave Abrahamsが
このアイデアに対して良い意見を述べていました。
私はこのディスカッションがもっと広い範囲で重要なことだと思ったので
この場で繰り返し強調したいと思います。
※
セマンティクスという言葉がよく出てきていて
うまく日本語にするのが難しいのでそのまま使っていますが
「意義」「役割」「目的」といった意味で使用します。
同様に
コンテキストは「文脈」「意味合い」という意味で使用します。
セマンティクスがProtocolの根本的な要素である
最初の意見はProtocolはただの構文の入れ物ではないという点です。
Protocolのセマンティクスはそのインターフェイスと全く同じレベルで重要なのです。
Xiaodi Wu:
Swiftでは、Protocolはただ単に特定のつづりを保証するものではなく、
同様に特定のセマンティクスを保証するものです。
Dave Abrahams:
加えて: これはStepanovが主張している
ジェネリックプログラミングの中核の原理でもあります。
そして:
Protocol(いわゆる概念)はただの構文の入れ物ではありません。
その機能にセマンティクスを追加することができない限り、
問題に対する有用なジェネリックな解決方法を書くことはできないでしょう。
これはx + x
を書ための何かを象徴するPlusable
を入れないのと同じ理由で
我々はDefaultConstructible
を入れるべきではないでしょう。
Equatableのセマンティクス
Equatable
と例として挙げます。
このAPIは最低限でたった一つのメソッドです。
public protocol Equatable {
/// Returns a Boolean value indicating whether two values are equal.
///
/// Equality is the inverse of inequality. For any values `a` and `b`,
/// `a == b` implies that `a != b` is `false`.
static func == (lhs: Self, rhs: Self) -> Bool
}
しかし、Equatable
に型を適合させるためには
このドキュメントにある
Protocolのセマンティクスに従って実装することも保証する必要があります。
簡単に言うと
等式は置換可能であることも意味する
等しいとみなされたどんな2つのインスタンスでも
その値に依存したどんなコードでも交換して利用することができます。
置換可能を維持するために==
はEquatable
型の全ての値を考慮に入れるべきである
例えば
struct Person {
let firstName: String
let lastName: String
}
があるとして
==
でfirstName
しか使用していなかった場合
Protocolのセマンティクスに据えてある契約に違反していることになります。
a==a
はいつもtrue(再帰性)でa==b
はb==a
(対称性)でa==b
かつb==c
ならばa==c
である(他動性)
不等は等式の逆で、!=
のカスタム実装をする際はa!=b
は!(a==b)
であることも意味している
クラスのインスタンスに関しては
インスタンスが同一(===
)という等式を使用するのが筋が通ります。
これは型の特徴に強く依存します。Jordan Roseのコメントを見てください。
Protocolは意味のある問題解決法を提供するべきである
Xiaodi Wu:
繰り返しになりますが、Protocolはただの構文ではなく、セマンティクスです。
これが意味することは、全く構文のないProtocolがあることが完全に合理的であるということです。
例えばprotocol MyProtocolWithSpecialSemantics { }
のように。
もう一つは、Protocolに必須の要件を完全に満たしていたとしても
自動で型に適合されないということです。
これはコンパイラがそのProtocolのセマンティクスを判断する方法がないからです。
Protocol、特にStandard Libraryに含まれるもの、の必須の定義は
それを基に「有用な」ジェネリックな問題解決法を含めることが必要不可欠であるべきです。
DefaultConstructible
に戻ります。
セマンティクスなしにただinit()
のみを保証したProtocolが
どんな興味深い問題解決法を提供できるでしょうか?
私には想像できません。
ジェネリックなデフォルト値というアイデアは意味があるのか?
または、このProtocolにどんなセマンティクスがあるのか考えてみてください。
有用な問題解決法は一貫した一連のセマンティクスの制約から出てくるものです。
追加のコンテキストのないジェネリックなinit()
という必須条件の問題は
T()
は異なるT
によって全然違う意味になります。
いくつかの型は直感的に「空」を表す。こういう場合は引数なしのイニシャライザと相性が良い
String()
は空の文字列を生成し
Array()
、Dictionary()
、Set()
は空のコレクションを生成する。
数値型やBool型の場合は不明瞭である
なぜBool()
はfalseではなくtrueなのか。これは意図的に感じます。
なぜ全ての数値型は0で初期化されるのか。
a + 0 == a
を考えると合理的に思えるが
a * 1 == a
も合理的に思える。
値ではなくオブジェクトの場合もある
UIView()
やThread()
は毎回固有のオブジェクトを生成しますが
「デフォルト値」で初期化されものの
その初期値にアクセスすることはできません。
Xiaodi Wu:
私が言えるのは
デフォルトが何であるかを明らかにしない限り
デフォルト値を使っても何もできません。
nil
を使って何もできないのも同様に。
nil
よりも有用にしたいならば
無制限のDefaultConstructible
を提供するのではなく
型に関するより特定の情報が必要になるでしょう。
ある型では意味のあるデフォルト値があるが
ある別の型ではそうではない。
もっとコンテキストを与えなければ
init()
には一貫したセマンティクスを与えることができません。
RangeReplaceableCollectionのセマンティクス
Standard Libraryにはinit()
が必須のProtocolがすでに存在しています。
RangeReplaceableCollectionです。
DefaultConstructible
との違いはProtocolにコンテキストがあってinit()
に意味があります。
我々は適合している型がコレクションであることを知っているためT()
は「空のコレクション」だとわかります。
さらにT()
がsomeCollection.removeAll()
と等しいことを主張できます。
RangeReplaceableCollection
のコンテキストはセマンティクスを考える上で
必要不可欠であるということは
このProtocolの必須条件init()
は別のProtocolに切り出すべきではなく
このProtocol内で定義されるべきだという事実を示しています。
Dave Abrahams:
DefaultConstructible
ではこのT
の値に関して何もわかりません。
なので期待通りの処理をすることができません。
もしこのデフォルト値で構築する条件がRangeReplaceableCollection
のような
より大きなProtocolの一部ならば
「これは空のコレクションを生成して、このインスタンスはremoveAllメソッドを呼んだインスタンスを同等です。」
ということができるでしょう。
init()
を自身のProtocolの中から外に切り出すという話ではありません。
Protocolのセマンティクス上の根本的機能の重要な部分を作るのにinit()が大きな役割を持つ
全てのProtocolにinit()
を含めようという話です。
[...]
意味のあるProtocolを構文上の値しか持たないような小さいProtocolに分割することは間違っています。
ここではそれが行われようとしているのではないかと疑問に感じています。
そして:
実装を抽出することはDRYという観点で重要で良いことです。
必要条件を抽出することは共通事項がある種のジェネリックプログラミンを可能にする時のみに行われるべきです。
実際、概念(つまりProtocol)を生成するのに必要条件をまとめることは
ジェネリックプログラミングの過程の重要なパートです。
競合したProtocol(同じ構文だけど違うセマンティクス)
このことが暗に意味するところとして
2つのProtocolの必要な項目の中で(全てまたは部分的に)同じ構文を持っているものの
異なるセマンティクスを持っている場合
一つの型が両方のProtocolに適合するべきではありません。
なぜならば両方のセマンティクスを同時に満たすような方法は存在しないからです。
Xiaodi Wu:
特に、Protocolがセマンティクス上の意味を持つということを非常に重要視しています。
Protocolがセマンティクスを持つという考えに厳粛にこだわり続けています。
なので私はこの提案が危険であると考えています。
なぜならばこの提案はこのProtocolの非常に重要な考えを明らかに否定していて
支持しているのはコンパイラではなく人々だけだからです。
セマンティクスなしではProtocolはリフレクションになってしまう
Xiaodi Wu:
もしアヒルのように歩き、アヒルのようにガーガー鳴くならば
それはおそらくアヒルだろう
ー ダックタイピングの規則より
これまで見てきたようにダックタイピングはSwiftにおいては好ましい考えではありません。
というのもある特定の機能を持っているオブジェクト(特定のAPIを実装を示すような)はセマンティクスについて
何も明示していないからです。
Xiaodi Wuは
提案されたDefaultConstructible
は
Swiftにより強力なリフレクションの機能のサポートが追加されればもっとうまく扱える
という意見を述べています。
まず、型が
init()
を有しているかどうかを知る方法が欲しいとあなたは思っているのです。
私にとってはそれはリフレクションのように聞こえてProtocolではないように聞こえます。
(反論として、リフレクションは型安全ではないという意見があります。)
Swiftは「ゼロ」というデフォルトの初期値を避ける
DefaultConstructible
への4番目の反論として
Swiftは「ゼロ」やデフォルト値で初期化しないという
Swiftのポリシーと衝突するというものです。
他の多くの言語とは異なりSwiftは変数のメモリをゼロにしません。
コンパイラはプログラマに全ての変数を明示的な値で初期化することを強要します。
この哲学に従うと
Swiftが他の言語でいうDefaultConstructible
のようなもの(例えばfactories)を実現しようとした場合
デフォルト値の構築を呼び出し側に任せるべきです。
Tony Allevatoはイニシャライザを関数として渡すことで
とてもきれいに実現することができるという意見を述べています。
T
型のインスタンスをジェネリックに提供するために必要なコードを何度か書く機会があり
Swiftが関数を第一級市民として、さらにイニシャライザを関数としてサポートしてくれているおかげで
() -> T
を引数として渡すようにしてT.init
を渡すことで実現することができています。
本文ここまで
まとめ
Protocolにはセマンティクスが重要であるということがわかりました。
Appleから提供されているProtocolも
セマンティクスを考えれば
なぜこういう定義になっているのか
がより見えやすくなるかもしれません。
また自身でProtcolを作成する際も
セマンティクスを考えることで
Protocolをどういう風にグルーピングして
どう分割していくかの指針にもなるのかなと感じました。
Protocolは色々な使い方ができますが
使う意義を考えてパワーをしっかり活用していきたいですね😄
何か間違いなどございましたらご指摘いただけましたらうれしいです🙇🏻♂️