書いて覚えるSwift入門

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

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
    }
}

より実践的なSequence

Sequenceが真の威力を発揮するのは、単一の値ではなく複数の値をまとめて扱うときにあるのは、.map().filter().reduce()の例のとおりです。Swiftにはすでに標準でArrayを持っていますが、たとえば5,000兆個のデータをArrayに読み込んでというのはメモリが足りなさ過ぎるでしょう。そういう場合にもSequenceに準拠した型としてそれを実装すれば、少しずつストレージから読んで処理するのも「いつものやり方」で実現できます。

ここではLispやHaskellでよく用いられている片方向リストを実装してみましょう。こんな感じですか。

enum List<T> {
    case Nil
    indirect case Pair(head:T, tail:List<T>)
}

最低限のアクセサーと……、

extension List {
    var car:T? {
        get {
            switch self {
            case .Nil: return nil
            case let .Pair(v, _): return v
            }
        }
        set {
            switch self {
            case .Nil: return
            case let .Pair(_, l):
                self = List.Pair(head:newValue!, tail:l)
            }
        }
    }
    var cdr:List<T>? {
        get {
            switch self {
            case .Nil: return nil
            case let .Pair(_, l): return l
            }
        }
        set {
            switch self {
            case .Nil: return
            case let .Pair(v, _):
                self = List.Pair(head:v, tail:newValue!)
            }
        }
    }
}

………イニシャライザーを用意しておきます。

extension List {
    init(fromArray: [T]) {
        self = fromArray
        .reversed()
        .reduce(.Nil) {
            List.Pair(head: $1, tail: $0)
        }
    }
    init(_ values:T...) {
        self.init(fromArray:values)
    }
}

この状態で、

var l = List(0,1,2,3)

とすれば確かにl0->1->2->3->Nilという具合に初期化されて、print(l)してみると、Pair(head: 0,tail: __lldb_expr_80.List<Swift.Int>.Pair(head: 1, tail: __lldb_expr_80.List<Swift.Int>.Pair(head: 2, tail: __lldb_expr_80.List<Swift.Int>.Pair(head: 3, tail:__lldb_expr_80.List<Swift.Int>.Nil))))なんて感じになっているのですが、これをSequenceにするにはどうしたらよいのでしょう? 先ほどの例のようにListIteratorを追加しても良いのですが、実はSwiftには次のような簡単な方法も用意されています。

extension List : Sequence {
    public func makeIterator() -> AnyIterator<T> {
        var list = self
        return AnyIterator {
            let v = list.car
            if let l = list.cdr {
                list = l
            }
            return v
        }
    }
}

こうしてからfor v in l { print(v) }してみると、確かに値を頭から取れていることがわかります。

あとはこれを利用して、Array化したり……、

extension List {
    var asArray:[T] {
        return self.map{$0}
    }
}

もっと見やすいよう文字列化したりするのは楽勝です。

extension List : CustomStringConvertible {
    var description:String {
        return "List("
          + self.map{"\($0)"}
          .joined(separator:", ")
          + ")"
    }
}
print(List(0,1,2,3)) // "List(0, 1, 2, 3)"

次回予告

今回はSwiftがSequenceプロトコルを通してどのように「順序」というものを抽象化しているかを見ていきました。しかし世の中は順序どおりにいくとは限らないもの。その場合には、どうしていくのか。

次回はCollectionプロトコルを通してそれを見ていきます。

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の実行環境(基礎編)

おすすめ記事

記事・ニュース一覧