Iterateeって何だ!?ってことで頭の悪い僕は、説明読んだだけではわからないので、実際にイジってみることにしました。
とりあえず、公式ページを読んでみて、なんとなく何をするものなのかは掴めた気がします。つまりIterateeってのは、とある入力があったとして、その入力を受け取って、その入力に対して行う処理を持っている箱のようなものなんだなってことです。入力がある度に、そのIterateeが使われるということのようです。例えば、数字を定期的に受け取って、これを足していくとか。いろいろな処理を部品化して、それぞれ組み合わせて使うことができますよっていう、きっと便利なものなんでしょう。
そしてさらに、お話はIterateeだけで終わらずに、Enumeratorってものもあります。これは、Iterateeに入力を与えるものらしいです。これもまた、いろいろ組み合わせていろんな与え方ができるようです。例えば、1から10までの数字を1秒置きにIterateeに渡すとか。
ということは、だいたいはIterateeとEnumeratorはペアで使うんですね。
ま、百聞は一見にしかずということで、実際にイジってみましょう。
入力を表示するだけのIteratee
まず、入力をコンソールに表示するだけのIterateeを作ってみましょう。
Iterateeは、トレイトになっていてfoldメソッドが抽象メソッドなので、これを実装すればインスタンスを作れそうです。
1 |
foldメソッドは、引数にfolderという関数を受け取ってますね。このfolderという関数は、Iterateeに入力を与えてくれるもので、folderに「入力を受け取った時の処理(関数)」を渡すというルールになっているようです。つまり、foldメソッド内で、「入力を受け取った時に処理する関数」を作って、それをfolderに渡す処理を書けばOKってことかな。そして、関数を受け取ったfolderは、その関数に入力を渡すようです。ちなみに、implicit ec: ExecutionContextは、Futureに関する処理を行うときにしばしば必要なもので、簡単にいうとスレッドプールです。Futureが使うスレッドをExecutionContextが決めます。Iterateeの本質とは関係ないので、おまじないみたいなものだと思ってください。とりあえず、ここまでの内容を書き下すと次のような感じなります。
1 2 3 4 5 | 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 | 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関数は、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 | 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 | // 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の結果をコンストラクタで受け取る。 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 |
1 2 | else new DoneIteratee( sum, input ) |
次に、MyIterateeに入力を渡すfolderを実装します。前回のfolderは、MyIterateeに1度しか入力を渡してなかったので、今回は入力値を増加させながら再帰的に入力を渡すように変更します。また、MyIterateeからStep.Done受け取るようになったので、その場合の処理も追加しました。
1 2 3 4 5 6 7 8 9 10 | 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 | // 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 | 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 | 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 | // 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 | 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 | 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 | 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を自力で実装する必要はなく、便利なヘルパーさんがいっぱいいるので、それらを有効活用しましょう!
0 件のコメント:
コメントを投稿