https://youtu.be/8H1VYeVGR8c
今回は、これまでの A Swift Tour
に戻って続きの Error Handling
を眺めていく回になりそうですけれど、その前に前回に言い残した 共変性と反変性
についてを先に見ていくことにしますね。とはいえ難しいところまでは把握できていないので、ひとまず言葉だけ知るような気持ちでそのうちの触り的なところを、Swift の挙動と照らしながら見ていけたらいいなと思ってます。よろしくお願いしますね。
———————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #68
00:00 開始 00:44 今回の発端 02:14 型パラメーターの関係性に影響を受ける型 04:07 標準では型パラメーターの関係性には影響を受けない 06:17 共変性と反変性 07:14 Swift では組み込みの言語仕様として備わっている 08:14 余談 11:37 共変性 12:18 共変性や反変性を明記しない 13:47 型を独自に定義するときは型パラメーターの関係性は考慮されない 15:37 辞書型の共変性 16:29 Set 型の共変性 18:52 辞書型のキーと値それぞれについての共変性 20:50 関数型の共変性 21:58 関数の戻り値における共変性 24:31 関数の引数における反変性 30:13 ここまでのおさらい 32:11 関数型の動的キャスト 37:17 関数型に対する動的キャスト 43:18 動的キャストと反変性は合致しない様子 46:59 オプショナルにおける共変性 48:58 クロージングと次回の展望 ————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #68
今日は少し脱線する形で、共変性と反変性についてお話しします。このテーマは、これまでに取り上げたプロトコルとも関連する内容です。せっかくの機会なので補足として紹介しようと思います。
この話の発端から説明すると、プロトコルAとそれに準拠した構造体Bがあるとします。これはプロトコルに限定されない話ですが、こういった場合にサブタイピングが関係してきます。たとえば、サブタイピングとは、プロパティ、例えばバリューがA型として扱われるが、その中にはB型を入れることができる、というような話です。プロトコル型に準拠したB型だから、A型としても扱えるという話ですね。
この特徴はさらに複雑な話に展開していきます。今日お話ししたいことをざっくりと言うと、A型とB型がサブタイピングの関係にあり、AのサブタイプとしてBが存在する。このときに、例えば配列としてのA型があり、その中にB型のインスタンスを入れることができるという性質です。これを具体的に表現すると、Array<A>
があったときに、その中にArray<B>
としてBのインスタンスを入れられるということです。
自動補完ではなく、アノテーションで明示的に配列のB型として右辺を規定し、左辺Array<A>
に入れることができる。このように、配列の要素にもサブタイピングの関係が影響しているわけです。これは一見何気なく使っていますが、実はかなり特殊なことで、例えばトラックとCがあって、これがジェネリクスとして規定されている場合、CがAとして規定されていると、CのB型を直接扱うことはできません。
具体的なコードを例にすると、以下のような形になります。
func someFunction() -> Array<B> {
// 中略
let arrayA: Array<A> = ...
let arrayB: Array<B> = ...
return arrayA // これはエラーになります
}
このように、AとBが継承関係やサブタイピングの関係にあったとしても、ジェネリクスのデータコンテナにはその関係が反映されません。しかし、配列に関してはサブタイピングの関係が反映されている点が特殊です。
この特性を共変性と呼びます。聞き慣れない言葉かもしれませんが、知っておくと理解が深まると思います。反変性はその逆の性質です。Swiftでは共変性と反変性の二つの性格が言語仕様として組み込まれていますが、プログラマーが明示的に扱えるものではないということです。
以上、共変性と反変性について簡単に説明しました。この概念は少し難しいですが、理解するとソフトウェア設計やプログラミングの応用に役立つと思います。 なので、プログラマーが自由に扱うことはできませんが、関係してくる性格なので、せっかくなので整理していきましょう。
ちょっとコメントに反応しますけど、Swiftと言えばInamiさんですね。InamiというTwitterアカウントで活動されていると思います。関数型プログラミングが好きなSwiftプログラマーの一人ですね。さて、この共変性と反変性についてですが、これはどういったものかというと、ある型とそのサブタイプがあったときの関係性の話です。
例えば、プロトコルで表現しましたが、クラスの継承関係でも同じです。ある型Aがあって、それを継承したBがあるとします。このようなとき、データコンテナについても同様にサブタイプの関係性が連動します。つまり、データコンテナBが別のコンテナAのサブタイプとして、中のデータの関係性に連動してサブタイプになるのが共変性です。これを「コバリアンス」や「コバリアント」と呼びます。
逆の関係、つまりデータコンテナAがデータコンテナBのサブタイプになる場合、これを反変性と呼びます。これは「コントラバリアンス」や「コントラバリアント」と表現されます。つまり、AのサブタイプとしてBがあるとき、その順序でサブタイプ性が成り立つのが共変、逆にBがAのサブタイプであるのに、コンテナではAがBのサブタイプとなるのが反変です。
ただし、説明だけでは分かりにくいので、具体例を見ていきましょう。まず共変性の例を見ていきます。Playgroundで配列A、つまりデータコンテナですが、コンテナが持っている要素Aがあったとき、AのサブタイプとしてBがあるとすると、コンテナAもコンテナBがサブタイプとして扱えるという感じです。
コメントでは「JavaやKotlinでは自動の共変反変がないから、プログラマーが修飾子を付けなければならない」とありますね。そうです。独自に共変や反変を規定できる言語では、プログラマーが「これはコバリアントですよ」とか「これはコントラバリアントですよ」と情報を添えることで、その性格を示せるようになります。Swiftではこういったキーワードは存在せず、Swiftが自動的に判断してくれます。
ただし、あらかじめ特定の状況に応じて、共変性や反変性が成立するように言語機能として組み込まれています。こういった仕組みのおかげで、Swiftでは自然に配列のようなデータコンテナーをスムーズに扱うことができます。配列や辞書型、セット型などがこの扱いに該当しますね。
実際に、ある値がディクショナリ(辞書型)としてあったとき、その場合も同様の扱いが可能です。 とりあえずキーを String
にしてみましょうか。それで、値として A
を扱うときに、キーがあって値として B
を入れる、というのを実際に辞書(Dictionary
)で試してみます。辞書のキーを String
、値を B
という型とすると、うまく入りますね。
Dictionary
の中で set
として A
型があったときに、これを別にインスタンス化しなくても問題ありません。わざわざ直すほどでもないですが、一応試してみます。エラーが出ましたね。プロトコルの問題でしょうか?おそらく Hashable
に関係したエラーですね。Hashable
に準拠させないといけないようです。
Hashable
プロトコルに準拠した場合、どうなるでしょうか?自動準拠はしないようですね。また別のエラーが出ました。associative type
になっているようですね。ちょっと厄介です。ということは、Hashable
に準拠したオブジェクト指向の方がやりやすそうですね。 X
を Hashable
にして、 Y
も同じようにやってみます。
すると、今度は Equatable
が準拠されていないと言われましたね。Equatable
も入れないといけません。そうですね、自動準拠のあたりがよく分からなくなってきました。今回のところはとりあえずこれで放っておきましょう。Hash
も実装しないといけないんですかね。ちょっと分からなくなってきましたが、hash
メソッドも書くだけ書いてみましょうか。
動きましたね。とりあえず動けば今回は OK です。そして、セットも大丈夫そうですね。しっかり動いてくれました。
では、せっかく Hashable
にしたので、キーとしても試してみましょう。キーとして Y
の辞書を使います。これで動くか確認してみましょう。動きますね。なので、ここから言えることとして、配列とセット、この2つは OK です。辞書においても、キーも値もそれぞれサブタイプ関係を意識して共変性を示してくれます。
他にデータコンテナがあるか考えてみます。Swift で一般的なものはこれくらいですかね。特別にこれらのデータコンテナは共変性を示してくれるということになります。
この共変性の話は、今見たデータコンテナだけでなく、関数型についても同様に独特な動きを見せてくれます。ある関数 F
が引数を取らずに戻り値として A
を返す場合、それに実際に入れる関数として戻り値の型が B
であっても成立します。これは、戻り値の B
が A
のサブタイプであるためですね。
具体的なコードで説明するとわかりやすいです。例えば、あるメソッドで戻り値がインスタンス B
であるとき、B
は A
のサブタイプなので、戻り値が A
でも受け手には問題ないわけです。つまり、受け手が戻り値 A
の関数であっても、B
を戻り値とする関数を受けることが可能です。
クロージャーの場合も同様で、戻り値が B
のクロージャーを戻り値 A
として受けることができます。このスライドを書いたときに混乱したことがあったのですが、試してみると理解できるかもしれません。
something
関数があって、戻り値 B
を返す関数があるとします。それを let g
に代入する際に something
を使ってもエラーは出ませんでした。つまり、問題なく受けることができるということですね。
ちゃんと動けば OK です。 とりあえずクロージャー式ではなくて、次のように書いてもシンプルで分かりやすくなります。とにかく、戻り値がAの関数型の変数に戻り値がBの関数型を入れられる、こういう風な共変性も示されます。これも言語の組み込みとして用意されているものになります。ただし、引数がある時にはこれが成り立ちません。つまり、引数の型がAでそれが戻り値Aを返すとき、戻り値の方を分かりにくくなるのでボイドにしますね。戻り値がボイドで引数がAの時に、これを引数Bで受け取るようにするとエラーになります。
具体的にはコンパイルエラーが出てきます。引数Bを受け取ってボイドを返す関数型を、引数Aを受け取ってボイドを返す関数型には入れられない、という型のミスマッチのエラーです。これは考えていけばとてもよくわかるのですが、実際に使おうとしたときです。
例えば f
という関数があり、A
型のプロパティがあるとします。この時に B
を入れようとすると直感的に分かりにくいかもしれませんが、関数 f
を呼ぼうとするときは、f
は A
型を引数として受け取るので、このコードは問題なく成立します。ただし、実際に処理を見ていくと、これを B
で受け取るとしましょう。ここで例えば何かをするときに B
型のサブタイプで処理をすることになります。
今のコードの例では、B
のインスタンスが A
型の変数に入っているので問題ないのですが、このインスタンス B
が B
のインスタンスであるとは限りません。別の型 C
が A
に準拠していて T
が渡ってくる可能性が出てきます。そうすると、引数 B
で受け取って B
型として扱うことはできません。ここは互換性がありません。しかし、B
型のパラメータを受け取って、その中で A
型として扱うのは問題ありません。
関数 f
はパラメータ B
を受け取って、そのパラメータを B
型として受け取りますが、実際の実装時にはそれを親の A
型として扱います。具体的な変数 B
が B
型として存在し、インスタンスが何かしら入っている場合、これを関数 f
として呼び出しに渡すと問題はありません。
おさらいすると、関数 f
はパラメータ B
を受け取って、その B
型として受け取っても、実際に実装時には親の型である A
型として扱うことに問題はありません。クロージャーを即座に入れる場合には分かりにくいかもしれませんが、A
を受け取って何らかの処理を A
として行う関数がどこかに定義されている場合、別の場面でクロージャーとして B
を受け取って何も返さないクロージャー型であっても問題はありません。
こうして、B
を受けてボイドを返す関数型が A
を受けてボイドを返す関数型のサブタイプとして扱えるのです。B
が A
のサブタイプであるので、この書き方では A
を受けてボイドを返す関数型が B
を受けてボイドを返す関数型のサブタイプになっているという性質が成り立ちます。これは「反転」と呼ばれ、正しいと思われます。
最後に、A
を返す関数と B
を返す関数が逆になっていることを確認します。どうでしょうか、これで説明ができましたでしょうか。 例えば、14行目のコードについて説明します。Bにサムシングを設定した場合、20行目で as
を使用して型キャストを試みます。ここでは型Bのままで良いです。20行目で Something as A
のように Void
を返す場合、アップキャストは可能ではないでしょうか。理論的には as
がそのまま使えるはずですが、実際にはエラーになります。やはり as!
を使う必要がある場面が出てきます。
具体的なエラー内容としては、サブタイプの関係が崩れるためです。Aはプロトコル型でしたよね。Bがそのプロトコルを遵守しています。しかし、Aで受けることができない場合、BにCが含まれる可能性が生まれるため、型の互換性が失われます。このような場合には静的キャストを試みることが一般的です。
今、14行目のジェネリックTがプロトコルAに準拠していますね。これを T: A
とし、それをジェネリックにすることでエラーを回避可能です。ただし、14行目の T: B
だとプロトコル型にはならず、クラス型であれば可能になります。このような混乱を避けるためにも、プロトコル型とクラス型の使い分けが重要になります。クラスで試すのであれば問題なさそうですが、ここでは避けておきましょう。
そうすることで、14行目が VA
の場合、Aをパラメーターとして渡せるため、結果として20行目の as
も成り立ちます。しかし、Aが入っている場合、BもAを満たしているため、中身としてAの振る舞いが可能となります。けれども、プロトコル型の場合、単純には行きませんよね。
動的キャストが必要な場合は、例えば as?
や as!
を使用します。具体的には、20行目で as!
を使用しても警告が出るかもしれませんが、これは要件を満たしています。このとき、動的キャストによる型のサブタイプ性を考慮しつつ、適切な型変換を行うことが可能です。もちろん、静的キャストも考慮する必要があります。このあたりの理解が進むにつれて、動的キャストの使用方法も自然に分かるようになるでしょう。
型の互換性を保ちながら、プロトコルやクラスを適切に使い分けてこそ、Swiftの能力を最大限に引き出せるはずです。 なので、ここはダイナミックキャスト失敗してnil
になる、という感じですね。
そうですね、そうですね。単純なサブタイピングとはちょっと違うところなんですか?
そうですね、ちょっと違う感じですね。混乱してきました。このサブタイピングを成立させるためにはOptional
にすると、A
になると思ったけど、ここもnil
になるんですね。ダメなんですね。A
が渡ってくれば絶対にOptional
ですよね。そうなんですね。
実際に変換したい場合はパラメータがA
になるのではなく、A?
(Aオプショナル)になるのはロジック的に自然です。完全にA
にキャストしてしまうと、違うタイプになってしまいます。サブタイピングのように親タイプから子タイプのキャストではなく、完全に違うタイプのキャストになってしまいます。オプショナルなのはここですか?
はい。もちろん、自動的にキャストしてくれないので、自動的に変えてあげないと、こういったことは成立しないですけど、難しいですね。なんか混乱してきてわからなくなってきました。そっかそっか、とりあえずここはいいけど、ここはダメとね。あー、as?
はダメなんですね。なるほど。
へー、そうなんですね。このas
は通るのに、as?
は通らないって、面白いですね。この特徴が見られると、少し混乱してきますが、反転という関係では、19行目と15行目が成り立つという感じですね。
はい、じゃあちょっとね、自分の中で混乱してるから、いろいろと聞かせてもらったことをちゃんと理解できてないことが多々あると思いますが、また復習して、何かあればお話させていただきますね。
はい、そんな感じで。とりあえず、この関数の戻り値はオッケーだけど、位数としては共変は成り立たなくて、逆に反転としてパラメータB
を受け取って、それをパラメータA
で受け取るものを入れることが可能になってますね。これが反転性の例になると思います。
あとオプショナルに対しても、さっきの話と共通するところがあるかどうか分かりませんが、オプショナルに対しても特徴的な関係が見られます。
例えば、Optional<B>
とOptional<A>
の関係が共変として成り立つ、つまりB
がA
のサブタイプであれば、Optional<B>
はOptional<A>
のサブタイプとして扱えます。これは配列などと同じようなデータコンテナの概念ですね。また、A
そのものがOptional<A>
のサブタイプとして扱えるというのは、ちょっとデータコンテナとは別の話になりますが、A
自体がOptional<A>
と共変性の関係が成り立つという特徴があります。
さらに、B
型、つまりA
のサブタイプであるB
であっても、Optional<A>
のサブタイプとして成り立つという、データコンテナに似た例が見られるわけです。このように、言語仕様によって組み込まれている特殊な動きがあるので、みんな何気なく使っていると思います。
プロトコルについても少し複雑になりますが、同じような特徴が見られます。この辺りは次回ゆっくり見ていくと理解が深まるかもしれません。意外と面白い、よく見る大事な特徴なので、次回もこの辺りを見ていきましょう。自分も復習できるのでちょうどいいかもしれません。
では、今日はちょうどいい時間なので、このくらいにしますね。何か質問等ありますか?
大丈夫ですか?
はい、自分も復習して、何か気づくことがあったら、また次回お話ししますね。今日の勉強会はこれで終わりにします。お疲れ様でした。ありがとうございました。