シーケンス (Sequence)
コレクションとともに、Kotlinの標準ライブラリにはもうひとつ別の型、シーケンス (Sequence<T>
) というものがある。
コレクションと異なり、シーケンスは要素を含まず、要素はイテレートされている間に生成される。
シーケンスはIterable
と同じ種類の関数を提供するが、
複数ステップに渡るコレクション処理に対しての異なるアプローチとして実装されている。
Iterable
の処理が複数ステップに渡ると、
それらはeagerで実行される(訳注:複数ステップがある時に一つ目のステップで全要素に対する処理が走ってから次のステップに進む事):
各ステップは完了してから結果を返す、それは中間のコレクションとなる。
その後のステップはこの中間のコレクションに対して行われる。
他方、シーケンスのマルチステップ処理は可能な時はlazyに実行される:
実際の計算は、全体の処理チェーンの結果が要求された時に発生する。
オペレーションの実行順序も異なる:Sequence
は一つの要素にすべての処理を行って、それから次の要素に行く。
他方、Iterable
は一つのステップをすべての要素に対して実行したあとに次のステップに行く。
つまり、シーケンスは中間ステップの結果を生成するのを避け、コレクション処理チェーンの全体のパフォーマンスを改善する。
しかしながら、lazyの特性からくるオーバーヘッドが追加されるので、
小さなコレクションを処理する時や、計算がより単純な時にはそちらのオーバーヘッドの方が大きくなる場合もある。
かくして、Sequence
とIterable
の両方を選択肢として考慮して、自分のその時の状況にとってどちらが良いかをその都度決めていく必要があります。
作成
要素から
シーケンスを作るには、sequenceOf()
関数を呼び出し、
その引数に要素を並べれば良い。
val numbersSequence = sequenceOf("four", "three", "two", "one")
Iterableから
もしすてにIterable
オブジェクト(例えばList
やSet
など)を持っていたら、
そのオブジェクトからシーケンスを、asSequence()
を呼ぶ事で作れる。
val numbers = listOf("one", "two", "three", "four")
val numbersSequence = numbers.asSequence()
関数から
シーケンスを作るもう一つの方法としては、要素を計算する関数で構築する、というものがあります。
関数に基づいてシーケンスを構築するには、
generateSequence()
を、関数を引数として呼び出します。
最初の要素を明示的に指定する事も出来ますし、指定しない場合は関数の結果が使われます。
提供した関数がnull
を返すとシーケンス生成は止まります。
以下の例のシーケンスは無限に続きます。
generateSequence()
を使って有限のシーケンスを作りたければ、あなたが必要とする最後の要素の後にはnull
を返す関数を渡せばよろしい。
チャンクから
最後に、要素を一つずつ、または任意のサイズのチャンクずつ作る関数、sequence()
関数というものがあります。
この関数は
yield()
と
yieldAll()
関数の呼び出しを含んだラムダ式を引数に取ります。
これらの関数はシーケンスのコンシューマに要素を返してsequence()
の実行をサスペンドします。
コンシューマから次の要素を要求されるまでサスペンドするのです。
yeild()
は要素一つを引数にとり、yieldAll()
はIterable
なオブジェクトかIterator
か別のSequence
を取ります。
yieldAll()
にわたすSequence
引数は無限でも構いませんが、
そのようなyieldAll()
の呼び出しは最後で無くてはなりません。それに続くコードは決して呼ばれることは無いのですから。
シーケンスのオペレーション
シーケンスに対するオペレーションは、そのステートに対する要求に応じて以下のグループに分類出来ます:
- ステートレスオペレーションは状態を必要とせず、個々の要素を独立して処理出来ます。例えば
map()
やfilter()
などです。 ステートレスオペレーションにはまた、要素を一つ処理するのに小さい定数量のステートを要求するものも含まれます。例えばtake()
やdrop()
などです。 - ステートフルオペレーションは、無視出来ないほどの量のステートを必要とするものです。通常は要素数に比例する量のステートとなります。
もしシーケンスのオペレーションが別のシーケンスを返すもので、しかもそれがlazyに生成される場合、それは中間(intermediate)と呼ばれます。
それ以外の場合、オペレーションは終端(terminal)と呼ばれます。
終端のオペレーションの例としてはtoList()
やsum()
などが挙げられます。
シーケンスの要素は終端のオペレーションでのみ取り出すことが出来ます。
シーケンスは複数回イテレート出来ますが、いくつかのシーケンスの実装は一回しかイテレートを許さないように制約を設けている場合もあります。 その場合は、それらのシーケンスのドキュメントに明示的にその旨の記述があります。
シーケンスを処理する例
例をもとにIterable
とSequence
の違いを見ていきましょう。
Iterable
単語のリストがあるとします。以下のコードは3文字以上の単語をフィルタしてその最初の4つの単語の長さを出力するものです。
このコードを実行すると、filter()
とmap()
関数がコードに出てくるのと同じ順番で実行される結果の出力を見ることになります。
まずフィルタ:
をすべての要素に対して見ることになり、それからフィルタされた後の要素に対して長さ:
が見られる。
そして最後の2行の出力が見られる。
以下はこのリストの処理がどう進むかを示した図です:
Sequence
では同じことをシーケンスで書いてみましょう:
このコードの出力は、filter()
とmap()
関数が、結果のリストを生成する時になって初めて呼ばれることを示しています。
つまり、最初に見るのは「3文字以上の〜」という行のテキストで、その次にシーケンス処理が始まります。
フィルタして残った要素に関しては、次の要素をフィルタする前にmapが実行されていることに注目してください。
結果のサイズが4に到達すると、処理は停止します。
なぜならそれがtake(4)
が返すことが出来る最大の要素数だからです。
シーケンスの処理は以下の図のように進みます:
この例では、シーケンスの処理は18ステップで済み、同じことをするリストの場合の23ステップより短いです。