書いて覚えるSwift入門

第27回静かなること型の如し

型の完全理解は可能か?

今回はSwift最大の特長であるプロトコル(protocol)を、総称型(generic type)と絡めつつ紹介します。そのためには、(type)とは何かをまず理解しておく必要があります。型とは何か、なんとも深淵そうな質問で、実際それだけでTAPLこと型システム入門という名著がまるごと1冊書けてしまうほどなのですが、本連載は「書いて覚える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)?C(++)?やJavaなどのコンパイル言語に慣れた人にとっては当たり前のこの挙動は、JavaScriptやPerlやPythonやRubyなどのスクリプト言語にとっては驚きの結果です。

node(JavaScript)
> var i = 0
undefined
> i == 0
true
> i == 0.0
true
perl -de 1
main::(-e:1): 1
  DB<1> my $i = 0

  DB<2> p $i == 0
1
  DB<3> p $i == 0.0
1
python
>>> i = 0
>>> i == 0
True
>>> id == 0.0
True
irb(ruby)
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が成立する理由は厳密にはそれぞれの言語で異なるのですが、Swiftで0 == 0.0が成立しない理由は明白です。型が一致しないからです。0Intという型で、0.0Doubleという型になります。そしてSwiftの型は静的。コンパイルの段階でどの変数(および定数)がどんな型なのかがあらかじめ決まっているので、0 == 0.0は実行すらさせてくれないというわけです。

なぜSwiftでは00.0は別々の型なのでしょう?

別の役割が期待されているからです。

たとえば割り算。IntDoubleでそれぞれ/してみましょう。

  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.2000000000000002。何が違うか。そう、余りです。

  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/で、精度一杯まで「割り続ける」代わりに「余り」を出さないのがDoubleの/。こういった区別がない言語では、==が楽な代わりにほかで苦労しています。たとえばJavaScriptにはDoubleに相当するNumberはあってもIntに相当する型はないので、Swiftの42 / 10に相当する演算は(42 / 10) ¦ 0などとしなければなりません。

引数をそのまま返すだけの簡単なお仕事

ここで、1番目に簡単な関数を考えてみましょう。ちなみに0番目に簡単な関数は何も引数を取らず何もしない関数で、Swiftならばこうなります。

func noop(){}

これが0番目なら、1番目は当然1つ引数をとってそれをそのまま返す関数になるでしょう。簡単ですね――動的言語なら。

JavaScript
function id(x){ return x }
// es6 ならもっと簡単に var id = (x)=>x;
Perl
sub id { @_ }
Python
def id(x):
  return x
Ruby
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 }
// ...

こういうのを繰り返し書かなければならないということでしょうか? やってることどころか{}の中身もまったく同じなのに?

ここで颯爽と登場するのが総称型(generictype)。次のように書いておけば……

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は総称関数(generic function)Tプレイスホルダー型(placeholder type)と言います。

得意なことは違うから

この何でもござれぶりを見ると、関数という関数を総称型で書きたくなってきますが、これは以前言った「本当に必要なとき以外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のことをプロトコル(protocol)といい、T:EquatableTという型はEquatableというプロトコルに準拠(conform)している」ことを表現します。

めでたし、めでたし?

――ちょっと待った! これは?

  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ではないのに……。

「それ自体はプロトコル準拠ではないけど、中身は準拠している」ということをSwift語で何と言えばいいのでしょうか?

こういうときに便利なのがswiftdoc.orgArrayDictionaryRangeが共通して準拠している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

これでDictionaryeq()できるようになりました。

オレオレプロトコルの書き方

それでは同様に、Collectionの中身を総和するsumというメソッドを書いてみましょうか。そのためにはCollectionIterator.Elementが演算+をサポートしていることをSwiftが知っていればよいわけですが、==Equatableと違って+Addableというプロトコルは見当たりません。

ならば定義してしまいましょう。

protocol Addable {
    static func +(_ l:Self, r:Self)->Self
}

これは、次のとおりに読めます。

> プロトコルAddableに準拠している型は、自分自身と同じ型を持つlrを受けて同じ型の値を返す+という二項演算子を持つ

IntDoubleといった数値型のみならずStringAddableに準拠しているのは明白ですが、残念ながらSwiftは良きに計らってくれません。プロトコルに準拠しているのだということをSwiftに教えるには、空の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
    }
}

要素をイテレートするのにずいぶんまだるっこしい方法を使っていますが、これはArrayRangeをそのまま拡張するのではなく、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...100).sum()のようにRangeまでまとめて拡張することはままなりません。

図1 実行画面
図1 実行画面

次回予告

というわけで今回もコード盛りだくさんでお届けしましたが、次回はWWDCの知見をなるべくお伝えしたうえで、次回にまたプロトコルについて続きを話します。

Software Design

本誌最新号をチェック!
Software Design 2022年9月号

2022年8月18日発売
B5判/192ページ
定価1,342円
(本体1,220円+税10%)

  • 第1特集
    MySQL アプリ開発者の必修5科目
    不意なトラブルに困らないためのRDB基礎知識
  • 第2特集
    「知りたい」⁠使いたい」⁠発信したい」をかなえる
    OSSソースコードリーディングのススメ
  • 特別企画
    企業のシステムを支えるOSとエコシステムの全貌
    [特別企画]Red Hat Enterprise Linux 9最新ガイド
  • 短期連載
    今さら聞けないSSH
    [前編]リモートログインとコマンドの実行
  • 短期連載
    MySQLで学ぶ文字コード
    [最終回]文字コードのハマりどころTips集
  • 短期連載
    新生「Ansible」徹底解説
    [4]Playbookの実行環境(基礎編)

おすすめ記事

記事・ニュース一覧