型の完全理解は可能か?
今回はSwift最大の特長であるプロトコル
0 == 0.0 // compile error
REPLで次のとおりに入力してみましょう。
var i = 0
i == 0
i == 0.0
macOSでは次のようになります。
Welcome to Apple Swift version 3.1 (swiftlang-802.0.53 clang-802.0.42). Type :help for assistance. 1> var i = 0 i: Int = 0 2> i == 0 $R0: Bool = true 3> i == 0.0 error: repl.swift:3:3: error: binary operator'==' cannot be applied to operands of type 'Int' and 'Double' i == 0.0 repl.swift:3:3: note: expected an argument list of type '(Int, Int)' i == 0.0
(Objective)
> var i = 0 undefined > i == 0 true > i == 0.0 true
main::(-e:1): 1 DB<1> my $i = 0 DB<2> p $i == 0 1 DB<3> p $i == 0.0 1
>>> i = 0 >>> i == 0 True >>> id == 0.0 True
irb(main):001:0> i = 0 => 0 irb(main):002:0> i == 0 => true irb(main):003:0> i == 0.0 => true
スクリプト言語で0 == 0.
が成立する理由は厳密にはそれぞれの言語で異なるのですが、0 == 0.
が成立しない理由は明白です。型が一致しないからです。0
はInt
という型で、0.
はDouble
という型になります。そしてSwiftの型は静的。コンパイルの段階でどの変数0 == 0.
は実行すらさせてくれないというわけです。
なぜSwiftでは0
と0.
は別々の型なのでしょう?
別の役割が期待されているからです。
たとえば割り算。Int
とDouble
でそれぞれ/
してみましょう。
1> var i = 42 i: Int = 42 2> i / 10 $R0: Int = 4 3> var d = 42.0 d: Double = 42 4> d / 10 $R1: Double = 4.2000000000000002
かたや4
、4.
。何が違うか。そう、
5> i % 10 $R2: Int = 2 6> d % 10 error: repl.swift:6:3: error: '%' is unavailable: Use truncatingRemainder insteadd % 10 Swift.%:2:13: note: '%' has been explicitlymarked unavailable here public func %(lhs: Double, rhs: Double) -> Double
整数の範囲で%
で出せるのがInt
の/
で、/
。こういった区別がない言語では、==
が楽な代わりにほかで苦労しています。たとえばJavaScriptにはDouble
に相当するNumber
はあってもInt
に相当する型はないので、42 / 10
に相当する演算は(42 / 10) ¦ 0
などとしなければなりません。
引数をそのまま返すだけの簡単なお仕事
ここで、
func noop(){}
これが0番目なら、
function id(x){ return x }
// es6 ならもっと簡単に var id = (x)=>x;
sub id { @_ }
def id(x):
return x
def id(x)
x
end
それではSwiftでは? Swiftは静的型言語
func id(_ x:Int)->Int { return x }
func id(_ x:Double)->Double { return x }
func id(_ x:String)->String { return x }
// ...
こういうのを繰り返し書かなければならないということでしょうか? やってることどころか{}
の中身もまったく同じなのに?
ここで颯爽と登場するのが総称型
func id<T>(_ x:T)->T {
return x
}
何でもござれです。
1> func id<T>(_ x:T)->T { 2. return x 3. } 4> id(0) $R0: Int = 0 5> id(0.0) $R1: Double = 0 6> id("") $R2: String = "" 7> id([0]) $R3: [Int] = 1 value { [0] = 0 } 8> id([0:""]) $R4: [Int : String] = 1 key/value pair { [0] = { key = 0 value = "" } }
ここでidは総称関数T
はプレイスホルダー型
得意なことは違うから
この何でもござれぶりを見ると、Any
型は使うべきではない」l == r
に相当するeq(l,r)
を同様に書いてみましょう。
1> func eq<T>(_ l:T, _ r:T)->Bool { 2. return l == r 3. } 4. error: repl.swift:2:14: error: binary operator'==' cannot be applied to two 'T' operands return l == r repl.swift:2:14: note: overloads for '==' exist with these partially matching parameterlists: (Any.Type?, Any.Type?), (UInt8, UInt8),/* 中略 */ (UnsafePointer<Pointee>, UnsafePointer<Pointee>) return l == r
なんかむちゃくちゃ文句言って来ましたよ。Any
型には==
はない」==
演算子はどんな型でもOKとはいかない以上、
func eq(_ l:Int, _ r:Int)->Bool {return l == r}
// ...
の時代に戻らなければいけないということでしょうか?
ここでいよいよプロトコルが登場します。要は==
演算子を持つ型」
func eq<T:Equatable>(_ l:T, _ r:T)->Bool {
return l == r
}
今度はうまくいきました!
1> func eq<T:Equatable>(_ l:T, _ r:T)->Bool { 2. return l == r 3. } 4> eq(0,0) $R0: Bool = true 5> eq(0.0,0.0) $R1: Bool = true 6> eq("","") $R2: Bool = true
このEquatable
のことをプロトコルT:Equatable
でT
という型はEquatable
というプロトコルに準拠
めでたし、
――ちょっと待った! これは?
4> [0]==[0] $R0: Bool = true 5> eq([0],[0]) error: repl.swift:5:8: error: argument type'[Int]' does not conform to expected type 'Equatable' eq([0],[0])
なぜ[0]==[0]
はうまくいくのにeq([0],[0])
はうまくいかないのでしょう? むしろ[0]==[0]
がうまくいくほうが不思議ではありませんか? Array
自体はEquatable
ではないのに……。
「それ自体はプロトコル準拠ではないけど、
こういうときに便利なのがswiftdoc.Array
やDictionary
やRange
が共通して準拠しているSequence
をよく見てみるとelementsEqualなるメソッドが存在するではありませんか。
func eq<T:Sequence>(_ l:T, _ r:T)->Bool
where T.Iterator.Element:Equatable
{
return l.elementsEqual(r)
}
このようにして、
4> eq([0],[0]) $R0: Bool = true 5> eq(0...1,0...1) $R1: Bool = true
うまくいったようですが、
7> eq([0:""],[0:""]) error: repl.swift:7:3: error: type '(key: Int,value: String)' does not conform to protocol 'Equatable' eq([0:""],[0:""])
Sequence
としてのDictionary<K,V>
のElement
は(K, V)
というタプル型で、Equatable
ではない、
さすれば……
func eq<K: Equatable,V: Equatable>
(_ l:[K:V], _ r:[K:V])->Bool
{
return l == r
}
これを実行してみると、
6> eq([0:""],[0:""]) $R0: Bool = true
これでDictionary
もeq()
できるようになりました。
オレオレプロトコルの書き方
それでは同様に、Collection
の中身を総和するsum
というメソッドを書いてみましょうか。そのためにはCollection
のIterator.
が演算+
をサポートしていることをSwiftが知っていればよいわけですが、==
のEquatable
と違って+
にAddable
というプロトコルは見当たりません。
ならば定義してしまいましょう。
protocol Addable {
static func +(_ l:Self, r:Self)->Self
}
これは、
> プロトコル
Addable
に準拠している型は、自分自身と同じ型を持つ l
とr
を受けて同じ型の値を返す+
という二項演算子を持つ
Int
やDouble
といった数値型のみならずString
もAddable
に準拠しているのは明白ですが、extension
を用います。
extension Int: Addable{}
extension Double: Addable{}
extension String: Addable{}
extension Array: Addable{}
……(中略)……
ここでプロトコルに準拠していない型にextension
をかけるとエラーで止まります。
extension Dictionary: Addable {} // error: type 'Dictionary<Key, Value>' doesnot conform to protocol 'Addable'
これで準備は完了。あとはCollection
を拡張するだけです。
extension Collection where Iterator.
Element:Addable {
func sum()->Iterator.Element? {
guard var v = self.first else {
return nil
}
var i = self.startIndex
i = self.index(after:i)
while i != self.endIndex {
v = v + self[i]
i = self.index(after:i)
}
return v
}
}
要素をイテレートするのにずいぶんまだるっこしい方法を使っていますが、Array
やRange
をそのまま拡張するのではなく、Collection
というプロトコルを拡張しているから。たとえばArray
だけであれば、
extension Array where Element:Addable {
func sum()->Element? {
guard var v = self.first else {
return nil
}
for i in 1..<self.count {
v = v + self[i]
}
return v
}
}
とより簡潔に書けますが、(0...
のようにRange
までまとめて拡張することはままなりません。

次回予告
というわけで今回もコード盛りだくさんでお届けしましたが、
本誌最新号をチェック!
Software Design 2022年9月号
2022年8月18日発売
B5判/
定価1,342円
- 第1特集
MySQL アプリ開発者の必修5科目
不意なトラブルに困らないためのRDB基礎知識 - 第2特集
「知りたい」 「使いたい」 「発信したい」 をかなえる
OSSソースコードリーディングのススメ - 特別企画
企業のシステムを支えるOSとエコシステムの全貌
[特別企画] Red Hat Enterprise Linux 9最新ガイド - 短期連載
今さら聞けないSSH
[前編] リモートログインとコマンドの実行 - 短期連載
MySQLで学ぶ文字コード
[最終回] 文字コードのハマりどころTips集 - 短期連載
新生「Ansible」 徹底解説
[4] Playbookの実行環境 (基礎編)