CLOVER🍀

That was when it all began.

Groovy-streamで遊ぶ

某言語を使っていると、とにかく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を積み重ねる実装になっているようです。あまり深くは追ってませんけど。

こんなところで。