書いて覚えるSwift入門

第29回 順序どおり問題を制する

この記事を読むのに必要な時間:およそ 3.5 分

Sequenceプロトコル

前回はWWDC特集だけあって、Swift 4を含め現行のSwift 3ではまだ実行できないコードも多く登場したのですが、今回は順序どおりすべてSwift 3で実行できるコードを書いていきます。

そう、順序。Swiftで順序といえばSequenceプロトコル。ArrayDictionaryRangeもすべてこのSequenceプロトコルに準拠しています。

コンピュータ言語における順序の歴史

コンピュータプログラミングとはものごとを順序よく片付けるために存在するといっても過言ではなく、たいていのプログラミング言語にはそのための構文糖衣が用意されています。いにしえの行番号付きBASICですら、こんな感じに。

10 FOR N=1 TO 30
20 LET F$ = ""
30 IF N MOD 3 = 0 THEN LET F$ = "Fizz"
40 IF N MOD 5 = 0 THEN LET F$ = F$ + "Buzz"
50 IF F$ = "" THEN PRINT N ELSE PRINT F$
60 NEXT N
70 QUIT

なぜ構文糖衣かといえば、⁠順序よくやる」は単に「終わりが来るまで次をやる」のと同じことだからです。C言語はこのあたりはっきりしていて、

int i;
for (i = 0; i < 42; i++) {
  /* ... */
}

は、

int i = 0;
while (i < 42) {
  /* ... */
  i++;
}

と等価であることがはっきりとわかります。

しかしより現代的な言語では「順序よくやれ」という場合に「いつ終わるのか」⁠次とは何か」を省略して書くのが流儀となっています。たとえばPerlなら同様の繰り返しは、

for $i (0..41) {
  # ...
}

と、⁠0から41まで」という言い方で、⁠0から1つずつ増やしていって42だったら終了」という言い方を避けています。

Swiftも、かつてはC言語風のforループが存在しました。

for var i = 0; i < 42; i++ {
  // …
}

しかしこれはSwift 2で廃止され、

for i in 0..<42 {
  // …
}

という「0から42の手前まで」⁠あるいは0...41と書いて「0から41まで」⁠という書き方に統一されました。

なぜそうなったかといえば、そのほうが直感的で間違いが減るから。終了条件で<と<=を違えてしまうというのは、今でもしばしば脆弱性の原因となったりもします。

それでもSwift 1にはC言語風のforが残っていたのははなぜでしょう? Sequenceプロトコルが未熟だったからです。このプロトコル一時期はSequenceTypeに改名されていたりと言語設計者も迷った形跡が見られますが、Swift 3にいたってそのあたりの「ぶれ」もなくなり、Swift 4のSequenceもSwift 3と互換です。

Sequenceの正体

それではSwiftにおけるSequenceとはなんなのか? ⁠書いて覚える」という視点では、次の2つが等価であることさえ覚えておけば良いでしょう。

for i in s {
    doSomething(with:i)
}
var t = s.makeIterator()
while let i = t.next() {
    doSomething(with:i)
}

つまり、.makeIterator()という「イテレーター」を返すメソッドを持ち、そのイテレーターに対して.next()を呼ぶことで、順序よく値を取り出し、.next()nilを返したらおしまい、というわけです。

それでは実際にIntSequenceにしてしまいましょう。

public struct IntIterator : IteratorProtocol {
    var count:Int
    init(_ count: Int) {
        self.count = count
    }
    public mutating func next() -> Int? {
        if count == 0 {
           return nil
        } else {
           defer { count += count < 0 ? 1 : -1 }
           return count
        }
    }
}
extension Int:Sequence {
    public func makeIterator() -> IntIterator {
        return IntIterator(self)
    }
}

こんなので実際に動くのでしょうか?

for i in 4 {
    print(i)  // 4, 3, 2, 1
}

動きました!

しかし、そのためにIntIteratorというStructを作るのもなんだかめんどうです。この場合は次のようにしてまとめられます。

extension Int:Sequence,IteratorProtocol {
    public mutating func next() -> Int? {
        if self == 0 {
            return nil
        } else {
            defer { self += self < 0 ? 1 : -1 }
            return self
        }
    }
}

それでもC言語のforに比べてめんどくさそうに思えます。しかし、わざわざSequenceに準拠するだけの価値は確かにあるのです。こうしておくことで、.map().filter().reduce()が無料で手に入るのです。

42.map{$0}          // [42,41,…,1]
42.map{$0 % 2 == 0} // [42,40,…,2]
42.reduce(0,+)      // 903

Swiftにおいて型をSequenceに準拠させるということは、RubyにおいてそのClassをEnumerableにすることに相当します。RubyにおけるEnumerableの発展を見れば、SwiftでSequenceに準拠させることがどれほどの利益をもたらすかをあらためて感じ取れるのではないでしょうか。

とはいえ、さすがにIntSequeceにするのはやり過ぎでしょう。RubyでもIntクラスはEnumerableではないのですから。しかしRubyのIntには.timesメソッドが存在します。SwiftのInt.timesは標準装備されていませんが、次のとおり簡単に追加できます。

extension Int {
    public var times:CountableRange<Int> {
        return 0..<self
    }
}

著者プロフィール

小飼弾(こがいだん)

1969年生まれ,東京都出身。元ライブドア取締役の肩書きよりも,最近はPokemon GOのガチトレーナーのほうが有名になりつつある……かもしれない永遠のエンジニアオヤジ。

活躍の場はIT業界だけでなく,サブカルからアカデミックまで多方面にわたり,ネットからの情報発信は気の向くまま毎日毎秒! https://twitter.com/dankogai,ニコニコチャンネルは,http://ch.nicovideo.jp/dankogai,blogはhttp://blog.livedoor.jp/dankogai/

当社刊行書籍は『小飼弾のアルファギークに逢ってきた』『小飼弾のコードなエッセイ』など。他にも著書多数。

コメント

コメントの記入