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