Sequenceプロトコル
前回はWWDC特集だけあって、Swift 4を含め現行のSwift 3ではまだ実行できないコードも多く登場したのですが、今回は順序どおりすべてSwift 3で実行できるコードを書いていきます。
そう、順序。Swiftで順序といえばSequence
プロトコル。Array
もDictionary
もRange
もすべてこの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
なぜ構文糖衣かといえば、
int i;
for (i = 0; i < 42; i++) {
/* ... */
}
は、
int i = 0;
while (i < 42) {
/* ... */
i++;
}
と等価であることがはっきりとわかります。
しかしより現代的な言語では
for $i (0..41) {
# ...
}
と、
Swiftも、かつてはC言語風のfor
ループが存在しました。
for var i = 0; i < 42; i++ {
// …
}
しかしこれはSwift 2で廃止され、
for i in 0..<42 {
// …
}
という0...
と書いて
なぜそうなったかといえば、そのほうが直感的で間違いが減るから。終了条件で<と<=
を違えてしまうというのは、今でもしばしば脆弱性の原因となったりもします。
それでもSwift 1にはC言語風のfor
が残っていたのははなぜでしょう? Sequence
プロトコルが未熟だったからです。このプロトコル一時期はSequenceType
に改名されていたりと言語設計者も迷った形跡が見られますが、Swift 3にいたってそのあたりのSequence
もSwift 3と互換です。
Sequenceの正体
それではSwiftにおけるSequence
とはなんなのか? 「書いて覚える」
for i in s {
doSomething(with:i)
}
var t = s.makeIterator()
while let i = t.next() {
doSomething(with:i)
}
つまり、.makeIterator()
という.next()
を呼ぶことで、順序よく値を取り出し、.next()
がnil
を返したらおしまい、というわけです。
それでは実際にInt
をSequence
にしてしまいましょう。
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
に準拠させることがどれほどの利益をもたらすかをあらためて感じ取れるのではないでしょうか。
とはいえ、さすがにInt
をSequece
にするのはやり過ぎでしょう。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)
とすれば確かにl
は0->1->2->3->Nil
という具合に初期化されて、print(l)
してみると、Pair(head: 0,tail: __
なんて感じになっているのですが、これを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 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の実行環境 (基礎編)