読者です 読者をやめる 読者になる 読者になる

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 オブジェクトを返すので、まずは IO オブジェクトの run メソッドで IterV オブジェクトを取得して、次に IterV オブジェクトの run メソッドで処理結果を取得します。

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);
            }
        });
    }
}

*1:Play2 の Iteratee 等とは違ってパターンマッチを使わなくても良いようになっています

*2:def step = {・・・ step(・・・) ・・・} のようにするとエラーが発生します