少年は言った。
「0から100までカウントアップしたい!!」
よし、じゃあ今日は0から100までの整数値を出力する、Sequenceの仲間たちを紹介するよ!
一緒にSequenceに強くなろう!
Sequenceのチカラ
まずはSequenceとは何かということと、そのパワーについて知りましょう。
Sequenceとは
Sequenceとは、標準ライブラリのの中にある以下のようなprotocolです。
public protocol Sequence {
public func makeIterator() -> Self.Iterator
}
makeIteratorを実装することによりSequenceに準拠することができます。Self.IteratorはIteratorProtocolを実装するクラスもしくは構造体で、makeIteratorは単にIteratorProtocolに準拠するインスタンスを返す関数です。IteratorProtocolとは以下のようなprotocolです。
public protocol IteratorProtocol {
public mutating func next() -> Self.Element?
}
Self.Elementはイテレートする任意の要素型です。IteratorProtocolはnext()を呼ぶたびに「次の要素」を返し、最後の要素の次にnilを返すように実装します。
Sequenceに準拠するといいこと
Sequenceに準拠することにより、contains
やforEach
, map
, filter
, reduce
などなど、多数の便利な関数を利用できるようになります。うまく使えば強力な独自クラスをつくることも可能です。
Sequenceとfor-in
さらに、SwiftではSequenceプロトコルに準拠することにより、for-inでイテレートできるようになります。標準ライブラリでよく使うArrayやDictionaryなどもSequenceプロトコルに準拠しているため、for-inでイテレートできるようになっています。以下の例はArrayをfor-inでイテレートしています。
let numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
for i in numbers {
print("\(i)") // 0, 1, 2...
}
さて、ここからは0から100までを出力する様々なSequenceを作る方法を紹介していきます。
カウントアップするSequenceを作る様々な方法
Closed Range Operatorを使う
おそらく最も一般的な方法はRange Operatorを使う方法です。
for i in 0...100 {
print("\(i)") // 0, 1, 2...
}
なにも問題ありません。シンプルで過不足無く美しいコードです。
0...100
はCountableClosedRangeインスタンスを生成します。これはstructであり、Sequenceに準拠しているためfor-inでイテレートできます。
SequenceとCollectionの違い
実際のところ、CountableClosedRangeは単なるSequenceではなく、さらに高機能なRandomAccessCollectionに準拠しています。SequenceとCollectionはfor-inの挙動において抑えておきたい違いが2点あります。
- Sequenceは一度イテレートすると二回め以降は同じようにイテレートできることが保証されていませんが、Collectionは何度もイテレート可能です
- SequenceはIteratorの実装によっては無限に続く可能性もありますが、Collectionは添字アクセスを可能にするため、有限であることが保証されています
詳しくは公式リファレンスの "Traversing a Collection" を参照してください。
https://developer.apple.com/reference/swift/collection
Sequenceを自分で実装する
SequenceとIteratorProtocolを実装する
自分でSequenceを実装する方法をご紹介します。
struct CountUp: Sequence, IteratorProtocol {
private var count = 0
mutating func next() -> Int? {
defer { count += 1 }
return count <= 100 ? count : nil
}
}
for i in CountUp() {
print("\(i)") // 0, 1, 2...
}
IteratorProtocolとSequenceを別々に実装することもできますが、こちらのほうが同時に実装できてシンプルですね。しかもこの場合makeIterator()
はデフォルト実装を利用できるため、自分で実装する必要はありません。
公式でもこの書き方が例として紹介されています。
https://developer.apple.com/reference/swift/sequence
AnySequenceを使う
AnySequenceを使うと、具体的なクラスを作ることなく独自処理のSequenceを実装することができます。
let countUp = AnySequence { () -> AnyIterator<Int> in
var count = 0
return AnyIterator {
defer { count += 1 }
return count <= 100 ? count : nil
}
}
for i in countUp {
print("\(i)") // 0, 1, 2...
}
AnySequence自体はSequenceの具体的な型を隠蔽して扱う(Type Erasure)ためのクラスですが、今回のように一時的な無名Sequenceを作る用途でも利用できます。初期化時にmakeIteratorの実装をクロージャで渡すようなイメージです。
StrideThroughを使う
StrideThroughというSequenceがあります。stride(from:through:by:)
関数を用いて、指定した閉区間をストライドで区切ったSequenceを作ることができます。
let countUp = stride(from: 0, through: 100, by: 1)
for i in countUp {
print("\(i)") // 0, 1, 2...
}
byに2を渡せば 0, 2, 4, 6... と1つ飛ばしにもできますし、stride(from: 100, through: 0, by: -1)
のようにマイナスを渡せば 100, 99, 98... と減っていくようにもできます。
また、半開区間をストライドで区切るStrideToと言うものあります。そちらはstride(from:to:by:)
を使います。StrideThroughと違い、toで指定した値は出力されません。
UnfoldSequenceを使う
UnfoldSequenceというSequenceがあります。カウントアップは以下のように書けます。
let countUp = sequence(first: 0) { prev -> Int? in
return prev >= 100 ? nil : prev + 1
}
for i in countUp {
print("\(i)") // 0, 1, 2...
}
prevは1つ前に出力した値です。UnfoldSequenceは1つ前の値を保持しており、1つ前の値から次の値を計算して出力しています。今回は単に1つ前の値に1を足して次の出力としています。
ただし、UnfoldSequence自体は単に1つ前の値だけを保持するだけのものではありません。より正確に言えば、UnfoldSequenceはイテレーションを通して状態を持ち、出力する要素を毎回計算するSequenceです。イテレーションを通して状態を持ち続けたい場合はsequence(state:next:)
という関数を使います。state
には自由な値を設定でき、イテレーションを通してアクセスすることができます。
UnfoldSequenceは使いどころが難しいですが、無限に続く数列を定義したり、木構造の親を辿っていくような再帰的な処理に使ったり、2つのイテレータを交互に出力したりなど、工夫次第で様々な用途で使えるようです。
まとめ
さまざまな方法でカウントアップしてみましたが、いかがでしたでしょうか。
Sequenceに強くなれたかな?