Kotlin の Sequence
オブジェクトの生成方法を7種紹介します。
(2019-04-06 1種追加して6種に。)
(2019-04-13 1種追加して7種に。)
この記事で最もお伝えしたいのは、最も強力でありながら最も知られていないと思われる、sequence
関数 を使う方法です。
ぜひそこをお読みいただいて、活用していただければと思います。
asSequence
拡張関数
Sequence
を使ったことがある方は皆さんご存知でしょう。
List
インターフェイスなどが継承している Iterable
インターフェイスや、 Array
や IntArray
などの配列系クラス。それらの拡張関数 asSequence
を使うことで、それらを Sequence
化できます。
val sequence = listOf('A', 'B', 'C').asSequence()
sequence.forEach { print(it) } // > ABC
なお、関数名は toSequence
ではなく asSequence
です。
asSequence
関数で生成した Sequence
オブジェクトには、元となるオブジェクトへの変更が反映されます。
val mutableList = mutableListOf('A', 'B')
val sequence = mutableList.asSequence()
mutableList += 'C'
sequence.forEach { print(it) } // > ABC
toList
関数などでは、元となるオブジェクトへの変更が反映されません。
この違いが関数名に現れているのでしょう。
sequenceOf
関数
引数で与えられた要素から Sequence
オブジェクトを生成します。
val sequence = sequenceOf('A', 'B', 'C')
sequence.forEach { print(it) } // > ABC
emptySequence
関数
空(要素数が 0)の Sequence
オブジェクトを生成する関数です。
シングルトンになっているため軽量です。
ですが、引数なしで sequenceOf
を使ってしまっても構いません。
要素数が 0 の場合は自動的に emptySequence()
が呼ばれるようになっています。
Sequence
インターフェイスの拡張関数
filter
や map
などの、Sequence
オブジェクトを加工して別の Sequence
オブジェクトを生成する、Sequence
インターフェイスの拡張関数です。
コレクション系の型の拡張関数と同じ感覚で使えます。
val sequence = sequenceOf(0, 1, 2)
.map { 'A' + it }
sequence.forEach { print(it) } // > ABC
Sequence
関数(フェイクコンストラクター)
Sequence
型はインターフェイスなのでコンストラクターを持ちませんが、このSequence
関数をコンストラクター代わりに使うことができます。
class CharIterator : Iterator<Char> {
private var char = 'A'
override fun hasNext(): Boolean = char <= 'C'
override fun next(): Char = char++
}
val sequence = Sequence { CharIterator() }
sequence.forEach { print(it) } // > ABC
Sequence
インターフェイスはメンバとして、Iterator
オブジェクトを返す iterator
関数だけを持っています。
Sequence
関数は引数として受け取った関数を、その iterator
関数の実体として使用します。
generateSequence
関数
要素を生成する関数1から Sequence
オブジェクトを生成します。
関数が null
を生成すると終端するため、要素に null
を含む Sequence
オブジェクトは生成できません。
また、生成した Sequence
オブジェクトは一度しか使えません。
このような制約があるものの、ある程度自由に手軽に Sequence
を作成できます。
var char = 'A'
val sequence = generateSequence {
if (char > 'C') null
else char++
}
sequence.forEach { print(it) } // > ABC
sequence.forEach { print(it) } // IllegalStateException をスローする。
sequence
関数
本記事で最もお伝えしたい方法です。
任意の Sequence
を簡単に作成できます。
val sequence = sequence {
var char = 'A'
while(char <= 'C') {
yield(char++)
}
}
sequence.forEach { print(it) } // > ABC
sequence.forEach { print(it) } // > ABC
使い方
sequence
関数の引数である関数(以下、 block
と表記)の中で yield
関数を呼び出すごとに、yield
関数に引数として渡した値が、生成した Sequence
オブジェクトの要素となります。
block
の処理が終了すると終端します。
yield
関数を呼び出して要素を生成すると、次の要素が要求されるまで block
の処理は停止します。
そのため、終わりがない数列を表す Sequence
オブジェクトを生成するような場合でも、必要な計算のみが行われます。
解説
block
は SequenceScope
クラスをレシーバーとする拡張関数です。
したがって、block
内では this.yield
(もしくは this.
を省略して yield
)とすることで、SequenceScope#yield
メソッドを呼び出せます。
yield
関数は中断関数(suspending function)です。
中断関数は、呼び出されてから処理を返すまでの間に処理を「中断」することができます。
「中断」すると、続きの処理は非同期で行われることになります。
中断関数は Kotlin 1.3 で正式に採用された Coroutines の機能に関連するものです。詳しくはこの辺りをご覧ください。→ Coroutines Overview
例:フィボナッチ数列
val fibonacci = sequence {
var a = 0
var b = 1
while (true) {
yield(b)
val c = a + b
a = b
b = c
}
}
fibonacci
.take(10)
.forEach { println(it) }
出力
``` 1 1 2 3 5 8 13 21 34 55 ```おわりに
以上、Sequence
オブジェクトの生成方法を6種紹介しました。
一通り網羅したつもりですが、他にもあればご教授ください。
-
ラムダ式を使う場合がほとんどでしょう。 ↩