某言語を使っていると、とにかくwhile/break/continueを避けたくなるものですが、Groovyにその言語のIterator.continually的なものがないかなーと思って探していたら、以下のものに辿り着きました。
Groovy-stream
http://timyates.github.io/groovy-stream/
https://github.com/timyates/groovy-stream
Groovyで、Streamを実現するためのライブラリのようです。
さっそく、チュートリアルを見つつ使ってみましょう。上記サイトのトップページにチュートリアルがあります。
あとは、Javadocを。
http://timyates.github.io/groovy-stream/javadoc/
依存関係の引き込みには、Grapeを使うことにします。現時点での最新版は、0.8.1です。チュートリアルが0.6.2と書いているのは、罠な気がしますね…。
@Grab('com.bloidonia:groovy-stream:0.8.1') import groovy.stream.Stream
import文としては、とりあえず上記があればよいみたいです。
使い方ですが、このStreamクラスのfromメソッドが起点になります。いくつかオーバーロードされたメソッドが存在しますが、まずはClojureを与えるものから。
def x = 1 assert [1, 2, 3] == Stream.from { x++ }.take(3).collect()
fromの中が繰り返し評価されますが、takeで3つ分だけに切り取り、その後collectメソッドでListに変換しています。
Stream#fromメソッドはStreamを返し、その他のStreamメソッド(上記ではtake)もやはりStreamを返します。StreamクラスはIteratorを実装しているので、適当なところでコレクションに変換するなりeachで回すなりするとよいでしょう。
Stream#fromは、コレクションを引数に取ることもできます。
assert [2, 4, 6, 8, 10] == Stream.from([1, 2, 3, 4, 5]).map { it * 2 }.collect()
mapメソッドは、Streamクラスのメソッドです。これは有限のStreamですね。
そういえば、先ほどの
Stream.from { x++ }
だと無限Streamになるわけです。
単一の値を与え、その繰り返しのStreamを作成することもできます。
assert [1, 1, 1, 1, 1] == Stream.from(1).take(5).collect()
やはり、これも無限Streamです。
キーとIterableのペアを与えることで、Mapを生成することもできます。
assert [[a: 1, b: 4], [a: 1, b: 5], [a: 2, b: 4], [a: 2, b: 5]] == Stream.from(a: [1, 2], b: [4, 5]).collect()
生成される組み合わせは、ネストしたforみたいな評価順になってますね。
その他、各種メソッド。
skipで指定された数分の要素を飛ばします。
assert [4, 5] == Stream.from([1, 2, 3, 4, 5]).skip(3).collect()
filterで、述語を満たすもののみ残します。
assert [2, 4] == Stream.from([1, 2, 3, 4, 5]).filter { it % 2 == 0 }.collect()
collateで、分割を行います。
assert [[1, 2, 3], [4, 5]] == Stream.from(1 .. 5).collate(3).collect()
collateの引数を増やすと、動きが変わります。
assert [[1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5], [5]] == Stream.from(1 .. 5).collate(3, 1).collect()
3つ引数を取るバージョンもあるみたいです。
その他の例は、チュートリアルや以下を見てください。
Map Functions
http://timyates.github.io/groovy-stream/map.html
依存関係もGroovyしかないし、良いのではないでしょうか?
その他、評価順の確認をしてみましょう。以下のようなテキストを用意。
languages.txt
Java Scala Groovy #Clojure Kotlin
これを読み込むコードを用意。「#」で始まる行は、コメントとしてスキップ。
※Groovy JDKで考えると、ちょっと冗長なコードです
new File('languages.txt').withReader('UTF-8') { reader -> Stream.from { reader.readLine() } .until { it == null } .filter { !it.startsWith('#') } .each { println("$it") } }
あ…until使った…。
実行すると、こういう結果になります。
Java Scala Groovy Kotlin
評価順を確認するため(ソースあんまり読んでない)に、こんなコードに変形。
new File('languages.txt').withReader('UTF-8') { reader -> Stream.from { println(' === generator'); reader.readLine() } .until { println(' === until'); it == null } .filter { println(' === filter'); !it.startsWith('#') } .each { println(' === each'); println("$it") } }
結果。
=== generator === until === filter === each Java === generator === until === filter === each Scala === generator === until === filter === each Groovy === generator === until === filter === generator === until === filter === each Kotlin === generator === until
なるほど、ループは増えない(操作はまとめて実行する)系ですね。さらっと見た限りは、Iteratorを積み重ねる実装になっているようです。あまり深くは追ってませんけど。
こんなところで。