型の完全理解は可能か?
今回は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の実行環境 (基礎編)
