Functional Java で Iteratee - take の実装
Functional Java の Iteratee を使って、以前試した Play2 の Iteratee (http://fits.hatenablog.com/entry/20130212/1360681996, http://fits.hatenablog.com/entry/20130216/1361027691) や RxJava (http://fits.hatenablog.com/entry/20130310/1362876374) と同等の処理を実装してみました。
ソースは http://github.com/fits/try_samples/tree/master/blog/20130518/
Functional Java の Iteratee に関して
Functional Java の Iteratee には、Iteratee への入力となる Iteratee.Input と Iteratee の処理を実装する Iteratee.IterV が用意されています。
Iteratee.Input は下記の 3タイプがあり、それぞれが匿名クラスとして実装されているのでファクトリメソッドを通して作成するようになっています。
apply メソッドに Empty・EOF・El の 3タイプそれぞれの処理内容を実装した P1 オブジェクトを渡す事で該当するタイプの処理を適用した結果を取得します。 *1
Iteratee.Input のタイプ
タイプ | 内容 | ファクトリメソッド |
---|---|---|
Empty | 入力データが空 | Input.empty() |
EOF | 終端に到達 | Input.eof() |
El | 入力データ有り | Input.el() |
Iteratee.IterV の 2つのタイプ Cont・Done も Input と同様に匿名クラスで実装されており、ファクトリメソッドを通して作成します。
run メソッドで処理を実施し処理結果を取得します。
Iteratee.IterV のタイプ
タイプ | 内容 | ファクトリメソッド |
---|---|---|
Cont | Iteratee の処理が継続中の状態。新しい Input を受け取って新しい IterV(Cont か Done)を返します | IterV.cont() |
Done | Iteratee の処理が完了した状態。処理結果を格納しています | IterV.done() |
動作内容としては下記のような処理が入れ子になるような感じです。
- Enumerator が Cont に Input を渡して、Done が返ってくれば処理終了、Cont なら Enumerator が次の Input を渡して・・・
IterV と Input の処理の流れは下記のようなイメージになると思います。
El で処理が完了となるか EOF が来れば Done で終了し、El で継続中となるか Empty が来れば Cont で処理が続く事になります。
ちなみに、ファイルの内容を読み込んで IterV に Input を渡す Enumerator 的な処理は IO クラスにメソッドが用意されており(enumFileLines 等)、drop や head 等の処理は IterV に用意されています。(ただし、take は用意されていないようです)
take 処理
Play2 の Enumeratee.take() や RxJava の Observable.take() のような take 処理は Functional Java 3.1 では用意されていないようなので、今回は自前で実装してみる事にしました。
take の処理内容は概ね下記のようになります。
- take の引数に与えられた数量の El を処理するまで Cont を返す
- 途中で EOF となるか、take が完了した際に Done (take した入力データを処理結果として格納) を返す
Groovy で実装
それでは Groovy で take を実装してみたいと思います。
まずは Cont を返さないと何も始まらないので take は Cont を返すようにします。
Cont には Input -> IterV という処理(与えられた Input の内容に応じて Cont / Done を返す)を実装した F オブジェクトを設定する事になります。
今回は Input を受け取って Cont / Done を返す処理をクロージャで実装し、"残りの take 数" と "take 結果のリスト" (今回は java.util.List を使いました)を引数として与えるようにしています。
ここで Empty・El・EOF それぞれの処理は以下のように実装しています。
Inputのタイプ | takeの状態 | 戻り値となるIterVのタイプ | 備考 |
---|---|---|---|
Empty | - | Cont | 残りの take 数そのままで step を実施するように設定 |
EOF | - | Done | take 結果のリストを設定 |
El | 完了(残りの take 数が 0 以下) | Done | take 結果のリストと未処理の入力データを設定 |
El | 継続中 | Cont | 残りの take 数を -1、take 結果のリストに入力値を追加して step を実施するように設定 |
また、Input.apply の引数に渡す型は下記のようになっています。(下記の E は入力データの型です)
Inputのタイプ | applyへの引数の型 |
---|---|
Empty | P1< IterV< E, List |
EOF | P1< IterV< E, List |
El | P1< F< E, IterV |
なお、クロージャ内で再帰的にクロージャを使っている関係上、def step を先に宣言して、後から実施を設定するようにしています。 *2
最後に take の動作確認として、drop と take を組み合わせた Iteratee にファイルの内容を処理させてみました。
今回はファイルを行単位で処理させるため IO.enumFileLines() を使っています。IO.enumFileLines() は IO
take_sample.groovy
@Grab('org.functionaljava:functionaljava:3.1') import fj.F import fj.P1 import fj.data.IO import fj.data.Option import static fj.data.Iteratee.* import java.nio.charset.StandardCharsets def take(int n) { def step step = { int count, List acc, Input s -> // Empty 時の処理内容(Input が空の場合) def empty = { IterV.cont( { step(count, acc, it) } as F) } as P1 // EOF 時の処理内容(終端に達した場合) def eof = { IterV.done(acc, s) } as P1 // El 時の処理内容(Input に値が設定されている場合) def el = { return { value -> if (count <= 0) { // take 結果リスト acc と未処理の入力 s を設定して Done IterV.done(acc, s) } else { // take した値を take 結果リスト acc に追加して Cont IterV.cont({ step(count - 1, acc << value, it) } as F) } } as F } as P1 s.apply(empty, el, eof) } IterV.cont({ step(n, [], it) } as F) } // (1) 1つ目を捨てて 2つ目から 3つ取り出す Iteratee 処理の組み立て def iter = IterV.drop(1).bind({ take(3) } as F) // (2) ファイルを行単位で (1) の処理を実施する IO<IterV> の取得 def ioIter = IO.enumFileLines(new File(args[0]), Option.some(StandardCharsets.UTF_8), iter) // (3) 処理の実行と結果の出力 ioIter.run().run().each { println "#${it}" }
実行結果は下記のようになります。
処理内容としては 1行目を無視して 2行目から 3行を取り出し先頭に # をつけて出力しています。
実行結果
> groovy take_sample.groovy sample.txt #サンプル2 #サンプル3 #サンプル4
使用したファイルの内容は以下の通りです。
sample.txt
サンプル1 サンプル2 サンプル3 サンプル4 サンプル5 サンプル6 サンプル7 サンプル8
Java で実装
同様の処理を Java SE 7 で実装してみました。(ただし、take 結果を溜めるリストの用途に java.util.List を使わず fj.data.List を使っています)
クロージャが使えない分、Groovy と比べると型まわりが相当ややこしくなっているので実用的とは言い難いかもしれません。
TakeSample.java
import fj.F; import fj.F3; import fj.Function; import fj.P; import fj.P1; import fj.Unit; import fj.data.IO; import fj.data.Option; import fj.data.List; import static fj.data.Iteratee.*; import java.io.File; import java.nio.charset.StandardCharsets; class TakeSample { public static void main(String... args) throws Exception { // (1) 1つ目を捨てて 2つ目から 3つ取り出す Iteratee 処理の組み立て IterV<String, List<String>> iter = IterV.<String>drop(1).bind(new F<Unit, IterV<String, List<String>>>() { @Override public IterV<String, List<String>> f(final Unit xe) { return take(3); } }); // (2) ファイルを行単位で (1) の処理を実施する IO の取得 IO<IterV<String, List<String>>> ioIter = IO.enumFileLines(new File(args[0]), Option.some(StandardCharsets.UTF_8), iter); // (3) 処理の実行と結果の出力 ioIter.run().run().foreach(new F<String, Unit>() { @Override public Unit f(final String line) { System.out.println("#" + line); return Unit.unit(); } }); } public static final <E> IterV<E, List<E>> take(final int n) { final F3<Integer, List<E>, Input<E>, IterV<E, List<E>>> step = new F3<Integer, List<E>, Input<E>, IterV<E, List<E>>>() { final F3<Integer, List<E>, Input<E>, IterV<E, List<E>>> step = this; @Override public IterV<E, List<E>> f(final Integer count, final List<E> acc, final Input<E> s) { // Empty 時の処理内容(Input が空の場合) final P1<IterV<E, List<E>>> empty = new P1<IterV<E, List<E>>>() { @Override public IterV<E, List<E>> _1() { return IterV.cont(new F<Input<E>, IterV<E, List<E>>>() { @Override public IterV<E, List<E>> f(Input<E> e) { return step.f(count, acc, e); } }); } }; // EOF 時の処理内容(終端に達した場合) final P1<IterV<E, List<E>>> eof = new P1<IterV<E, List<E>>>() { @Override public IterV<E, List<E>> _1() { return IterV.done(acc, s); } }; // El 時の処理内容(Input に値が設定されている場合) final P1<F<E, IterV<E, List<E>>>> el = new P1<F<E, IterV<E, List<E>>>>() { @Override public F<E, IterV<E, List<E>>> _1() { return new F<E, IterV<E, List<E>>>() { @Override public IterV<E, List<E>> f(final E value) { if (count <= 0) { // take 結果リスト acc と未処理の入力 s を設定して Done return IterV.done(acc, s); } else { // take した値を take 結果リスト acc に追加して Cont return IterV.cont(new F<Input<E>, IterV<E, List<E>>>() { @Override public IterV<E, List<E>> f(Input<E> e) { return step.f(count - 1, acc.snoc(value), e); } }); } } }; } }; return s.apply(empty, el, eof); } }; return IterV.cont(new F<Input<E>, IterV<E, List<E>>>() { @Override public IterV<E, List<E>> f(Input<E> e) { return step.f(n, List.<E>nil(), e); } }); } }