型の完全理解は可能か?
今回はSwift最大の特長であるプロトコル(protocol)を、総称型(generic type)と絡めつつ紹介します。そのためには、型(type)とは何かをまず理解しておく必要があります。型とは何か、なんとも深淵そうな質問で、実際それだけでTAPLこと『型システム入門』という名著がまるごと1冊書けてしまうほどなのですが、本連載は「書いて覚えるSwift入門」。実際に書いていくことにしましょう。
0 == 0.0 // compile error
REPLで次のとおりに入力してみましょう。
macOSでは次のようになります。
(Objective)?C(++)?やJavaなどのコンパイル言語に慣れた人にとっては当たり前のこの挙動は、JavaScriptやPerlやPythonやRubyなどのスクリプト言語にとっては驚きの結果です。
スクリプト言語で0 == 0.0
が成立する理由は厳密にはそれぞれの言語で異なるのですが、Swiftで0 == 0.0
が成立しない理由は明白です。型が一致しないからです。0
はInt
という型で、0.0
はDouble
という型になります。そしてSwiftの型は静的。コンパイルの段階でどの変数(および定数)がどんな型なのかがあらかじめ決まっているので、0 == 0.0
は実行すらさせてくれないというわけです。
なぜSwiftでは0
と0.0
は別々の型なのでしょう?
別の役割が期待されているからです。
たとえば割り算。Int
とDouble
でそれぞれ/
してみましょう。
かたや4
、かたや4.2000000000000002
。何が違うか。そう、余りです。
整数の範囲で「割り切る」代わりに「余り」も%
で出せるのがInt
の/
で、精度一杯まで「割り続ける」代わりに「余り」を出さないのがDoubleの/
。こういった区別がない言語では、==
が楽な代わりにほかで苦労しています。たとえばJavaScriptにはDouble
に相当するNumber
はあってもInt
に相当する型はないので、Swiftの42 / 10
に相当する演算は(42 / 10) ¦ 0
などとしなければなりません。
引数をそのまま返すだけの簡単なお仕事
ここで、1番目に簡単な関数を考えてみましょう。ちなみに0番目に簡単な関数は何も引数を取らず何もしない関数で、Swiftならばこうなります。
これが0番目なら、1番目は当然1つ引数をとってそれをそのまま返す関数になるでしょう。簡単ですね――動的言語なら。
それではSwiftでは? Swiftは静的型言語(大事なことなので何度も繰り返します)。関数を定義するときには、引数と戻り値を明示しなければなりません。ということは……
こういうのを繰り返し書かなければならないということでしょうか? やってることどころか{}
の中身もまったく同じなのに?
ここで颯爽と登場するのが総称型(generictype)。次のように書いておけば……
何でもござれです。
ここでidは総称関数(generic function)、T
はプレイスホルダー型(placeholder type)と言います。
得意なことは違うから
この何でもござれぶりを見ると、関数という関数を総称型で書きたくなってきますが、これは以前言った「本当に必要なとき以外Any
型は使うべきではない」と同様な意味でムチャ振りです。それを実感するため、今度はl == r
に相当するeq(l,r)
を同様に書いてみましょう。
なんかむちゃくちゃ文句言って来ましたよ。「Any
型には==
はない」と前回も言いましたが、==
演算子はどんな型でもOKとはいかない以上、総称しようがないのです。ということは、
の時代に戻らなければいけないということでしょうか?
ここでいよいよプロトコルが登場します。要は「==
演算子を持つ型」というのを何とか表現できればいいのですよね? これでどうだ?
今度はうまくいきました!
このEquatable
のことをプロトコル(protocol)といい、T:Equatable
で「T
という型はEquatable
というプロトコルに準拠(conform)している」ことを表現します。
めでたし、めでたし?
――ちょっと待った! これは?
なぜ[0]==[0]
はうまくいくのにeq([0],[0])
はうまくいかないのでしょう? むしろ[0]==[0]
がうまくいくほうが不思議ではありませんか? Array
自体はEquatable
ではないのに……。
「それ自体はプロトコル準拠ではないけど、中身は準拠している」ということをSwift語で何と言えばいいのでしょうか?
こういうときに便利なのがswiftdoc.org。Array
やDictionary
やRange
が共通して準拠しているSequence
をよく見てみるとelementsEqualなるメソッドが存在するではありませんか。
このようにして、実行してみると
うまくいったようですが、これでもまだ完璧ではありません。
Sequence
としてのDictionary<K,V>
のElement
は(K, V)
というタプル型で、これがEquatable
ではない、と。
さすれば……
これを実行してみると、
これでDictionary
もeq()
できるようになりました。
オレオレプロトコルの書き方
それでは同様に、Collection
の中身を総和するsum
というメソッドを書いてみましょうか。そのためにはCollection
のIterator.Element
が演算+
をサポートしていることをSwiftが知っていればよいわけですが、==
のEquatable
と違って+
にAddable
というプロトコルは見当たりません。
ならば定義してしまいましょう。
これは、次のとおりに読めます。
> プロトコルAddable
に準拠している型は、自分自身と同じ型を持つl
とr
を受けて同じ型の値を返す+
という二項演算子を持つ
Int
やDouble
といった数値型のみならずString
もAddable
に準拠しているのは明白ですが、残念ながらSwiftは良きに計らってくれません。プロトコルに準拠しているのだということをSwiftに教えるには、空のextension
を用います。
ここでプロトコルに準拠していない型にextension
をかけるとエラーで止まります。
これで準備は完了。あとはCollection
を拡張するだけです。
要素をイテレートするのにずいぶんまだるっこしい方法を使っていますが、これはArray
やRange
をそのまま拡張するのではなく、Collection
というプロトコルを拡張しているから。たとえばArray
だけであれば、
とより簡潔に書けますが、(0...100).sum()
のようにRange
までまとめて拡張することはままなりません。
次回予告
というわけで今回もコード盛りだくさんでお届けしましたが、次回はWWDCの知見をなるべくお伝えしたうえで、次回にまたプロトコルについて続きを話します。
- 第1特集
MySQL アプリ開発者の必修5科目
不意なトラブルに困らないためのRDB基礎知識
- 第2特集
「知りたい」「使いたい」「発信したい」をかなえる
OSSソースコードリーディングのススメ
- 特別企画
企業のシステムを支えるOSとエコシステムの全貌
[特別企画]Red Hat Enterprise Linux 9最新ガイド
- 短期連載
今さら聞けないSSH
[前編]リモートログインとコマンドの実行
- 短期連載
MySQLで学ぶ文字コード
[最終回]文字コードのハマりどころTips集
- 短期連載
新生「Ansible」徹底解説
[4]Playbookの実行環境(基礎編)