Iterateeって何だ!?ってことで頭の悪い僕は、説明読んだだけではわからないので、実際にイジってみることにしました。
とりあえず、公式ページを読んでみて、なんとなく何をするものなのかは掴めた気がします。つまり
Iterateeってのは、とある入力があったとして、その入力を受け取って、その入力に対して行う処理を持っている箱のようなものなんだなってことです。入力がある度に、その
Iterateeが使われるということのようです。例えば、数字を定期的に受け取って、これを足していくとか。いろいろな処理を部品化して、それぞれ組み合わせて使うことができますよっていう、きっと便利なものなんでしょう。
そしてさらに、お話は
Iterateeだけで終わらずに、
Enumeratorってものもあります。これは、
Iterateeに入力を与えるものらしいです。これもまた、いろいろ組み合わせていろんな与え方ができるようです。例えば、1から10までの数字を1秒置きに
Iterateeに渡すとか。
ということは、だいたいは
Iterateeと
Enumeratorはペアで使うんですね。
ま、百聞は一見にしかずということで、実際にイジってみましょう。
入力を表示するだけのIteratee
まず、入力をコンソールに表示するだけの
Iterateeを作ってみましょう。
Iterateeは、トレイトになっていて
foldメソッドが抽象メソッドなので、これを実装すればインスタンスを作れそうです。
1 | def fold[B]( folder: Step[E, A] => Future[B] )( implicit ec: ExecutionContext ): Future[B] |
foldメソッドは、引数に
folderという関数を受け取ってますね。この
folderという関数は、
Iterateeに入力を与えてくれるもので、
folderに「入力を受け取った時の処理(関数)」を渡すというルールになっているようです。つまり、
foldメソッド内で、「入力を受け取った時に処理する関数」を作って、それを
folderに渡す処理を書けばOKってことかな。そして、関数を受け取った
folderは、その関数に入力を渡すようです。ちなみに、
implicit ec: ExecutionContextは、
Futureに関する処理を行うときにしばしば必要なもので、簡単にいうとスレッドプールです。
Futureが使うスレッドを
ExecutionContextが決めます。
Iterateeの本質とは関係ないので、おまじないみたいなものだと思ってください。とりあえず、ここまでの内容を書き下すと次のような感じなります。
1 2 3 4 5 | // Iterateeが行う処理をfold関数に定義する。 def fold( folder: .. ) = { def func = <入力を受け取り、それを処理する関数> folder( func ) } |
上記のコードの
funcを実装すればよいってことになります。
funcの引数は入力です。これは
Input.Elという型です。では、戻り値は何でしょう。
folderの型を調べてみると、
Iterateeになってますね。入力を処理した結果を返すんじゃねーのか!?そうなんです、処理結果を返すんじゃなくて、
Iterateeなんです。つまり、次の処理を返しているんですね。次に入力があった時にする処理を
Iterateeに詰め込んで返すんです。その
Iterateeは、また次の
Iterateeを返し、そのまた次も....というように次から次へ処理していけるようになってるんですね。
ということで、ひたすらコンソール出力を繰り返す
Iterateeを作ってみましょう。
ここで作る
Iterateeは、
Int型のデータを入力として受け取って、
Int型のデータを生成する
MyIterateeという名前の
Iterateeです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // 入力をコンソール出力するだけのIterateeを定義 class MyIteratee extends Iteratee[Int, Int] { def fold[B]( folder: Step[Int, Int] => Future[B] )( implicit ec: ExecutionContext ): Future[B] = { // funcの実装 // 入力を受け取り、次のIterateeを返す def func( input: Input[Int] ): Iteratee[Int, Int] = input match { case Input.El( e ) => println( e ) new MyIteratee // とりあえず、他の入力は無視する case _ => ??? } // funcをStep.Contに包んで渡す folder(Step.Cont(func _)) } } |
funcが、
MyIterateeを返すことで、何回も
printlnが実行されるようにしています。
folderに
funcを渡すとき、
funcを
Step.Contで包んでます。これは、
funcは、次の処理をする
Iterateeを返しますよってことを
folderに教えているんです。この他にも
Step.Done、
Step.Errorがあります。
funcが次の処理を返さない時は、
Step.Doneで包みます。
Iterateeができたので、この
Iterateeに入力を与えて実行してみましょう。
Iterateeに入力を与えるのは、
folderという関数でした。この
folderという関数を定義して、
foldに渡してあげればいいんですね。
folderは、
Iterateeが生成した処理(
MyIterateeでいうところの
func)に入力を渡します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // folder関数の定義。 // folder関数は、Iterateeに入力を与える役割を担う。 // 引数のstepは、Iterateeから受け取る。MyIterateeでは、Step.Contを渡している。 def folder( step: Step[Int, Int] ): Future[String] = step match { // Step.Contからfuncを取り出す。 // funcというのは、MyIterateeで定義した入力を受け取って処理する関数。 case Step.Cont( func ) => // funcに入力(Input.El(1))を渡す // funcは新たなMyItrateeを返すが、ここでは無視する。 val next = func( Input.El(1) ) // "done"という文字列を返して終了 Future("done") // とりあえず他のStepは無視する case _ => ??? } |
folderは、
Stepを受け取ります。
MyIterateeでは、
folderに
Stepのサブクラスである
Step.Contを渡してますから、パターンマッチングで
Step.Contとして受け取ります。
Step.Contは、
funcを持っているので、
funcを取り出します。この
funcというのは、
MyIterateeで定義していて、入力を受け取って、その入力を処理する内容を記述したものでした。したがって、
funcは、入力を受け取るので、ここでは、
1(
Input.El(1))を渡します。
1を受け取った
funcは、それをコンソールに出力し、
MyIterateeを返します。
funcが返した
MyIterateeは、次に入力があった時に実行する処理を格納していますが、ここでは、簡単のために1回しか入力を行わないことにしますので、
folder関数内では、
MyIterateeを受け取りません。そして、
folderの戻り値である
Futureを返します。
では、
MyIterateeを実行してみましょう。
1 2 3 4 | // MyIterateeを実行する。 val result = (new MyIteratee).fold( folder ) // '1'と'done'が出力される println( Await.result(result, 5.second) ) |
Iterateeを実行するには、
foldメソッドを適用します。
結果として、
MyIterateeが
1をコンソール出力し、
folderの戻り値の
"done"をコンソール出力しています。
ちなみに、
Awaitは、
Futureが完了するまで、現スレッドで待機するというものです。
Thread#joinみたいなもんです。
1から10までの和を計算してみる
ここまでで、
Iterateeを定義してから実行するまでの流れはだいたい掴めた気がします。では、次はもうちょっと複雑なことをやってみたいと思います。前の
Iterateeは、単に入力をコンソールに出力するだけの簡単なものでした。しかも、1回だけ。今回は、1から10までの和を計算する
Iterateeを作ってみたいと思います。前回より複雑になるところは、10回だけ
Iterateeを繰り返し終了するというところと、1つの
Iterateeの結果を次の
Iterateeに渡すというところでしょう。まず、「計算を終了させる」ということを考えてみましょう。前回の例では、
fold関数内で
Step.Contを
folder関数に渡していました。この意味は、「この
Iterateeは、処理した後も続きを処理する
Iterateeを返しますよ」っていうことを
folderに伝える役割を果たしていました。
folderは、そのことによって続きの
Iterateeを受け取り、次の入力を渡せるのでした。このように
Iterateeが
Step.Contを
folderに渡し続けることで、繰り返し処理ができるのでした。では、その処理を終了するにはどうするのでしょう。前述したように
Stepには、
Step.Cont以外にも
Step.Doneというのがあります。これは、文字通り「この
Iterateeは、次を返しません。これで終わりです。そしてこれが結果です。」っていうことを
folderに伝えます。
Step.Doneを受け取った
folderは、
Step.Doneが持っている結果を取り出し、それを返します。ということは、計算終了時に
folderに
Step.Doneを渡す
Iterateeを作れば大丈夫そうです。さっそく、このような
Iterateeを定義してみましょう。
1 2 3 4 5 6 7 | // folderにStep.Doneを渡すIterateeの定義。 // Step.Doneは計算結果が必要なので、コンストラクタで計算結果を受け取る。 class DoneIteratee( sum: Int, input: Input[Int] ) extends Iteratee[Int, Int] { def fold[B]( folder: Step[Int, Int] => Future[B] )( implicit ec: ExecutionContext ): Future[B] = { folder(Step.Done(sum, input)) } } |
この
DoneIterateeの定義自体は簡単ですね。
Step.Doneを
folderに渡しているだけです。ただ、結果を受け渡しできるように、コンストラクタで結果を受け取るようにしています。これで、繰り返し処理を終了することができるようになりました。
次に、
MyIterateeを、足し算して結果を次の
Iterateeに渡して、その
Iterateeが前の結果に入力を足し算して、、、というように次々と足し算できるように改良してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // 入力の和を計算するIteratee。 // 前のIterateeの結果をコンストラクタで受け取る。 class MyIteratee( sum: Int ) extends Iteratee[Int, Int] { def fold[B]( folder: Step[Int, Int] => Future[B] )( implicit ec: ExecutionContext ): Future[B] = { // funcの実装 def func( input: Input[Int] ): Iteratee[Int, Int] = input match { case Input.El( e ) => // 入力が10以下の場合は、前の結果と入力を足して、 // 次のIterateeに渡す if( e <= 10 ) new MyIteratee( sum + e ) // 入力が10を超えた場合は、終了する。 // DoneIterateeがStep.Doneをfolderに渡す。 else new DoneIteratee( sum, input ) case _ => ??? } // funcをStep.Contに包んで渡す folder(Step.Cont(func _)) } } |
前回の
MyIterateeからの変更点は、コンストラクタで初期値(=1つ前に実行した
Iterateeの計算結果)を受け取るようにしたことです。
1 | class MyIteratee( sum: Int ) |
この初期値に
MyIterateeの
foldで処理した結果を足して、次の
MyIterateeに渡します。もうひとつの変更点は、入力を受け取った後の計算処理です。この
Itrateeの目的は、1から10までの和を計算することなので、入力を受け付けるのは10までにして、それ以外は
DoneIterateeを返すようにしました。
1 2 | if( e <= 10 ) new MyIteratee( sum + e ) else new DoneIteratee( sum, input ) |
これで入力を足しあげる
MyIterateeの完成です。
次に、
MyIterateeに入力を渡す
folderを実装します。前回の
folderは、
MyIterateeに1度しか入力を渡してなかったので、今回は入力値を増加させながら再帰的に入力を渡すように変更します。また、
MyIterateeから
Step.Done受け取るようになったので、その場合の処理も追加しました。
1 2 3 4 5 6 7 8 9 10 | def folder( n: Int )( step: Step[Int, Int] ): Future[Int] = step match { case Step.Cont( func ) => // Iterateeに入力を渡して、次に実行するIterateeを受け取る。 val next = func( Input.El( n ) ) // 入力を1つ増加して、次のIterateeを実行する。 next.fold( folder( n + 1 ) ) // Doneを受け取ったら、入力をやめて結果を返す。 case Step.Done( sum, _ ) => Future( sum ) case _ => ??? } |
それでは、この
MyIterateeを実行してみましょう。
1 2 3 | val result = (new MyIteratee(0)).fold( folder(1) ) // 55が出力される println( Await.result(result, 5.second) ) |
「55」と出力されたので、うまく動いているようですね。
さて次は、これを少し改良してみましょう。和を計算するのは変わらないんですが、
Iteratee側から
Step.Doneを返して終了するのはやめて、入力が終了するまで計算を続けるという仕組みに変更したいと思います。というのも、入力の終了を検知して、何かを処理する方法も知っておきたいからです。
MyIteratee側で変更する点は、入力
Inputのパターンマッチの部分の分岐を増やすことです。前回では、
Input.Elしか判別していませんでしたが、今回は
Input.EOFも判別するようにします。
1 2 3 4 5 6 7 8 9 10 11 12 | // Input.EOFを受け取るまで計算を続けるように変更 class MyIteratee( sum: Int ) extends Iteratee[Int, Int] { def fold[B]( folder: Step[Int, Int] => Future[B] )( implicit ec: ExecutionContext ): Future[B] = { def func( input: Input[Int] ): Iteratee[Int, Int] = input match { case Input.El( e ) => new MyIteratee( sum + e ) // この部分を追加 case Input.EOF => new DoneIteratee( sum, Input.EOF ) case _ => ??? } folder(Step.Cont(func _)) } } |
次に
folderの実装です。変更点は入力が10を超えたら
Input.EOFを
Iterateeに渡すことだけです。
1 2 3 4 5 6 7 8 9 | def folder( n: Int )( step: Step[Int, Int] ): Future[Int] = step match { case Step.Cont( func ) => // 入力が10を超えたらInput.EOFを返すように変更 val input = if( n <= 10 ) Input.El( n ) else Input.EOF val next = func( input ) next.fold( folder( n + 1 ) ) case Step.Done( sum, _ ) => Future( sum ) case _ => ??? } |
そして実行!
1 2 3 | val result = (new MyIteratee(0)).fold( folder(1) ) // 55が出力される println( Await.result(result, 5.second) ) |
「55」が出力されました。OKです!
もうちょっと汎用的に!?
さあ、次はどうしましょう?
MyIterateeと
folderをもうちょっと汎用的に使えるように改良したいですね。特に
folderは、1から10までの入力しか生成できないですからね。使い道があまりないですね。
MyIterateeは、和を取るだけじゃなくて、他の計算もできるように変更します。つまり、足し算をしている部分を何かを計算する関数に置き換えます。
sum + e =>
f(sum, e)この関数
fは、コンストラクタで渡すようにしましょう。
class MyIteratee( sum: Int, f: (Int, Int) => Int )ついでに、型もパラメータ化しちゃいましょう。
Int =>
A変更後の
MyIterateeは、次のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 | // 何かの処理する関数をコンストラクタに渡す。 class MyIteratee[A]( sum: A, f: (A, A) => A ) extends Iteratee[A, A] { def fold[B]( folder: Step[A, A] => Future[B] )( implicit ec: ExecutionContext ): Future[B] = { def func( input: Input[A] ): Iteratee[A, A] = input match { // sum + e を f(sum, e)に変更 case Input.El( e ) => new MyIteratee( f(sum, e), f ) case Input.EOF => new DoneIteratee( sum, Input.EOF ) case _ => ??? } folder(Step.Cont(func _)) } } |
では、
folderはどうのように汎用的にしましょう? 今までは、1から10までの数字しか入力できませんでしたが、様々な型の羅列を入力として扱えたら、少しは汎用的になるんじゃないでしょうか?とういことで、ある型のリストを渡すと、そのリストの先頭の要素から順次
Iterateeに渡すことができる
folder関数を作ってみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // 型Aのシーケンスを渡す。 def folder[A]( xs: Seq[A] )( step: Step[A, A] ): Future[A] = step match { case Step.Cont( func ) => // シーケンスの先頭と残りの要素を抽出する val (input, tail) = if( xs.nonEmpty ) (Input.El( xs.head ), xs.tail) else (Input.EOF, Nil) // 先頭の要素をIterateeに渡す val next = func( input ) // 残りの要素を次のIterateeに渡す next.fold( folder( tail ) ) case Step.Done( sum, _ ) => Future( sum ) case _ => ??? } |
では、これらを使って1から10までの和を計算する
Iterateeを実行してみましょう。
1 2 3 4 5 6 | // Iterateeの生成 val iteratee = new MyIteratee[Int](0, (sum, x) => sum + x) // 1から10までのシーケンスを生成して、Itarateeに渡す val result = iteratee.fold( folder(1 to 10) ) // 55が出力される println( Await.result(result, 5.second) ) |
無事に「55」が出力されました!
さて、いままで長々と
Iterateeをイジって来ましたが、実はですね、これらのことを行ってくれる便利なヘルパー関数が既に用意されているんですね。まぁ最初からそれを使えば早いんですが、
Itrateeを使うためのお勉強ということでご勘弁を。では、それらを使って、1から10までの和を計算する処理を書いてみると次のようになります。
1 2 3 4 5 6 7 8 | // 和を計算するIterateeの生成 val iteratee = Iteratee.fold[Int, Int](0) { (sum, x) => sum + x } // 1から10までを順次入力するEnumerator(folder関数のようなものを生成) val enum = Enumerator.enumerate(1 to 10) // 実行 val result = enum.run( iteratee ) // 55が出力される println( Await.result(result, 5.second) ) |
ここで、
Enumeratorというのが登場していますが、これは
folder関数の役割とほぼ同じことをしてくれます。そして、
Enumeratorの
runメソッドが
Iterateeを実行してくれます。
まとめ
やっぱり、文章読むだけじゃなくて、実際に自分でイジって動かしてみないと掴めないものってありますよね。ここでやったことだけでは不十分だと思いますが、
Itrateeと
Enumeratorが何をどうやっているのか、だいたい分かったような気がします。
ただ、実際には、
Iterateeや
Enumeratorを自力で実装する必要はなく、便利なヘルパーさんがいっぱいいるので、それらを有効活用しましょう!