Swift でのジェネレータの取扱いや遅延評価については,ymyzk 先生の『Swift でジェネレータを作ったり、遅延評価してみる』において解説されているが,2015年5月の情報といささか古く,Swift のヴァージョンも1.2だった頃の記事なので,改めて書くことにした.
2017年7月16日現在,Xcode 9.0 beta 上では Swift 4.0 がサポートされているが,手元の環境は Xcode 8.3.3 であるため,Swift 3.1 をベースに解説する.Apple の先走りで,ドキュメントのリンク先が一部 Swift 4 に関するものになっている部分があるが,本記事の理解にあたって本質的な違いはないはずである.
これまでの流れ#
- Ruby の Enumerator でジェネレータを作ったり、遅延評価してみる - blog.ryota-ka.me
- Python でジェネレータを作ったり、遅延評価してみる – ymyzk’s blog
- ECMAScript 6 でジェネレータを作ったり、遅延評価してみる – ymyzk’s blog
- Rust でジェネレータを作ったり、遅延評価してみる - blog.ryota-ka.me
- Swift でジェネレータを作ったり、遅延評価してみる – ymyzk’s blog
- PHP でジェネレータを作ったり遅延評価してみる - たにしきんぐダム
- Scala でジェネレータを作ったり、遅延評価してみる - たにしきんぐダム
- Perl 6 でジェネレータを作ったり、遅延評価してみる - blog.ryota-ka.me
- Vim script でジェネレータを作ったり、遅延評価してみる - blog.ryota-ka.me
IteratorProtocol
#
A type that supplies the values of a sequence one at a time.
と説明されるプロトコルで,かつては GeneratorType
と呼ばれていたものである.stdlib/public/core/Sequence.swift
で以下のように*1定義されている.
associated type として Element
という型をひとつ持つ protocol で,イテレータが次に yield する値を Element?
型*2で返す next()
メソッドを持っている.また,next()
メソッドには mutating
キーワードが付いているので,この際にデータ型の内部状態として持つ値は破壊的に変更してもよい*3.
試しに,next()
を呼び出す度,自然数を順に返す Nat
を定義してみよう.
struct Nat: IteratorProtocol {
var n = 0
mutating func next() -> Int? {
defer { n += 1 }
return n
}
}
var nat = Nat()
print(nat.next()) // Optional(0)
print(nat.next()) // Optional(1)
print(nat.next()) // Optional(2)
print(nat.next()) // Optional(3)
print(nat.next()) // Optional(4)
ここで defer statement
というものが登場しているが,これは,defer
が宣言されたスコープを抜ける際に,与えられた code block を実行してくれるものである.言語機構として yield
を持つ言語ならば,yield した後に値をインクリメントするといった処理が行えるが,構造体とメソッドしか持たない場合はそうもいかない.そこで,このような解決策を取っている.余談ではあるが,Swift 3 からは昔ながらのC言語スタイルの ++
(および --
) 演算子が削除された.
Sequence
#
Sequence
は,かつては SequenceType
と呼ばれていたもので,
A type that provides sequential, iterated access to its elements.
と紹介されており,データ型をこのプロトコルに適合させると,for-in
によるループ操作ができるようになる.データ型を Sequence
プロトコルに適合させるためには, イテレータを返す makeIterator()
メソッドを実装すればよいが,既に ProtocolIterator
に適合している場合はもっと簡単で,単に Sequence
に適合していることを宣言すればよい.これは以下のような宣言で実現されている*4.
/// A default makeIterator() function for `IteratorProtocol` instances that
/// are declared to conform to `Sequence`
extension Sequence where Self.Iterator == Self {
/// Returns an iterator over the elements of this sequence.
@_inlineable
public func makeIterator() -> Self {
return self
}
}
Sequence
は associated type として,IteratorProtocol
に適合するような Iterator
を持っているのだが,この Iterator
が自分自身のときには, makeIterator()
が self
を返すようなデフォルト実装が与えられている.
自然数の列は無限に長い*5ので,for-in
ループでイテレーションを行うと停止せず,困ってしまう.そこで,与えられた自然数 n について,0 から n までの有限列を返す Upto
イテレータを定義した上で,for-in
の挙動を確かめてみよう.
struct Upto: IteratorProtocol, Sequence {
let n: Int
var curr = 0
init(_ n: Int) {
self.n = n
}
mutating func next() -> Int? {
defer { curr += 1 }
return curr <= n ? curr : nil
}
}
for k in Upto(4) {
print(k) // 0, 1, 2, 3, 4 が順に表示される
}
あまりにも馴染みがある記法なので特に目新しさはないが,自分で定義したデータ型が for-in
ループで使用できていることがわかる.
遅延評価#
Sequence
は lazy
という instance property を持っているが,これは以下のようなシグネチャで表現されている.
extension Sequence {
/// A sequence containing the same elements as this sequence,
/// but on which some operations, such as `map` and `filter`, are
/// implemented lazily.
///
/// - SeeAlso: `LazySequenceProtocol`, `LazySequence`
public var lazy: LazySequence<Self> { get }
}
これは Self
を generic parameter として持っている LazySequence
を返す.これが Sequence
の遅延版であり,map
や filter
などのメソッドが遅延評価されるように実装されている.これを用いて,いつも通りではあるが,フィボナッチ数列のそれぞれの要素を自乗し,奇数のもののみ抜き出したのち,先頭から10個の要素を取り出してみよう.
struct Fib: IteratorProtocol, Sequence {
var (a, b) = (0, 1)
mutating func next() -> Int? {
defer { (a, b) = (b, a + b) }
return a
}
}
let xs: AnySequence<Int> = Fib().lazy
.map({ $0 * $0 })
.filter({ $0 % 2 != 0 })
.prefix(10)
print(Array(xs)) // [1, 1, 9, 25, 169, 441, 3025, 7921, 54289, 142129]
ここで AnySequence
という型が登場しているが,これは Sequence
に 型消去 (type erasure) を施したもので,残念なことにこれがないと型推論がおかしくなって死ぬ.というのも,コンパイルには成功するが,メソッド呼び出しが何故か正格な方に解決されてしまい,大きな数を扱おうとして illegal hardware instruction で死ぬ.なんとも情けない話である.
参考資料#
- IteratorProtocol - Swift Standard Library | Apple Developer Documentation
- Sequence - Swift Standard Library | Apple Developer Documentation
- AnySequence - Swift Standard Library | Apple Developer Documentation
- The Swift Programming Language (Swift 4): Statements
- The Swift Programming Language (Swift 4): Generics
- GitHub - apple/swift-evolution: This maintains proposals for changes and user-visible enhancements to the Swift Programming Language.
脚注#
*1: ただしコメント部は除く.
*2: 返すことができる値がない場合には nil
を返す
*3: かつては associated type declaration のために typealias
キーワードが用いられていたが,Swift 2.2 からは associatedtype
キーワードに変更されている.
*4: https://github.com/apple/swift/blob/5256d774eaf774ec1e3c566aa8047764a1124837/stdlib/public/core/Sequence.swift#L625-L633
*5: もちろんメモリを考慮すればこの限りではない.