ドキュメントの少なさなどからiterateeがどういうものなのかよく分からないという人も多いようです。いうわけなので、iterateeを易しく解説してみようと思います。HandleベースのI/Oはもともと手続き型言語の世界からやってきたもので、HaskellのAPIとして考えたとき十分に使いやすいとは言えません。その理由としては、composableでない関数型でない格好良くない!などがあります。I/Oというものもあります。ような関数的でとても綺麗なインターフェースで入出力処理を行うことができます。I/Oにはとても致命的ないくつかの問題点があるのです。I/Oが終わると同時にリソースを開放できない。GCを待つ必要がある。これはネットワークソケットなどの限られたリソースの場合は致命的。エラーハンドリングが困難。エラーの発生も遅延されるので、pureなコード内でI/Oエラーが発生するかもしれません。I/Oは使うべきではないとする意見もあるようです*9。そこでこれらの問題を解決した、次のようなAPIが望まれます。(リソース管理など)ハイパフォーマンスiterateeでは、これを逆転の発想で実現します。Handleのインターフェースは、データが必要になる度にユーザがデータを取りに行くという、いわばpull式のインターフェースですが、iterateeでは、データを受け取って処理するpush式のインターフェースになります。iterateeとはiterateeはfoldlのインターフェースで説明されます。Haskellプログラマならfoldlには親しみ深いと思います。foldlは3つの引数を取ります。初期値アキュームレータデータ列です。foldlはデータ列をアキュームレータに合成関数を用いて供給することにより最終出力を得ます。アキュームレータと合成関数を合わせて、iterateeと呼ぶことにします。すると、foldlは"iteratee"を用いて入力データを"イテレート"する、と考えることができます。入力例を生成し、iterateeに与えるものをenumeratorと呼ぶことにします。foldlでは単なるリストですが、我々の目的であるI/Oならば、Handleから入力例を得て、それをiterateeに渡す働きをすることになります。してこのような抽象化にするのでしょうか。これはひとえに計算の合成のためです。iterateeの合成2つのiterateeを合成して大きなiterateeをつくるというのを考えることができます。1つ目のiterateeを走らせて計算が完了したら、残りの入力列で2つ目のiterateeを走らせます。enumeratorの合成1つ目のenumeratorの要素に続けて2つ目のenumeratorの要素が続くようなenumeratorが考えられます。実装してみる具体的なコードを見たほうが理解も速いと思いますので、実際にiterateeを実装していくことにします。データストリームとiterateeのデータ構造を定義します。a)StreamGが入力ストリームを表しますが、一般的なストリームの定義とは異なります。ストリーム全体を保持するのではなく、ただ1つ(もしくは0個)の要素を保持します。ストリーム全体は[StreamG]として表されます。これはEOFの情報を持たせたいためです。iterateeを表しているのがIterVです。IterVはfoldlにおけるiterateeに対応するものですが、foldlの概念とは少し異なります。それはストリーム全体を消費しなくても計算を終了できる所です。Doneが終了した状態を現しており、計算結果と最後の要素を消費したかを持っています。Contは計算途中であることを表し、続きの計算を持っています。これを動作させるために、IterVにデータを供給するためのenum関数(enumeratorを表す)と、最終結果を取り出すためのrun関数を作る必要があります。iterateeには次の3つのタイプのものがあります。enum関数はこれらをハンドルできなければなりません。有限個の要素を消費し、Doneになるストリーム全体を消費し、EOFが来るとDoneになる(例えばsumのようなもの)決してDoneにならないenum関数は基本的にfoldlのような関数になりますが、Doneをハンドルしなければいけないところが異なっています。加えて、未完了のiterateeにEOFを送って結果を取り出すrun関数を作ります。具体的なiterateeの例を、よくあるリストの操作を定義することで見ていきます。先頭要素を消費して、先頭要素を返す。EOF先頭要素を消費せず先頭要素を返す。EOF要素をn個消費する。EOF要素の数を数える。EOF合成このようなiterateeの定義に対して、iterateeの合成を考えることができます。実際のところ、IterVはモナドであり、ApplicativeやFunctorにもできます。合成の意味は前の章で説明したとおりです。bindは1つめのiterateeを実行して、残りの入力列で2つめのiterateeを実行するiterateeを返します。i2)こうして我々はiterateeをHaskellの標準的な方法で合成できるようになりました。drop1keep1は、入力を1文字捨てて、次の文字を返すiterateeです。alternatesは入力列を1つ置きに、先頭5つまで取り出すiterateeです。[2,4,6,8,10]モナドの導入さて、ここまでiterateeを実装して来たわけですが、前の章の実装はpurelyな世界に閉じていて何もI/Oをしていません。I/Oをできるようにするため、enumeratorをモナドに対応させてやることにします。i)モナド対応版のenumeratorの型がEnumeratorMです。先程のenum関数と同じく、IterVを引数に取りIterVを返しますが、EnumeratorがI/Oをできるようにモナドに包まれています。実際のenumeratorの例がenumHandleとenumFileです。それぞれハンドルとファイルの文字要素を列挙します。これらのenumeratorは次のような重要な安全性を持っています。ハンドルがリークしないHandleがiterateeに渡らないので、どこかにエスケープしてしまうことがない。bracketによって例外が起きても安心。完了後すぐにハンドルを閉じることができるiterateeが遅延評価されないので、すぐに閉じることができます。enumHandleがEOFをiterateeに送らないのは重要です。enumeratorがEOFを送らないことによって、enumeratorの連結が可能になります。EOFを送るのはrunの仕事になります。lengthモナディックiterateeI/Oのうち、入力はこれでできるようになりましたが、出力はどうでしょう。iterateeをモナドに対応させることによりこれは可能になります。enumeratorより多少ややこしいです。IterVMになって、Iteratee型が増えています。Iterateeが、モナド対応版iterateeで、IterVMは内部的な型です(IterVMのコンストラクタが2つあるのでIterateeの内部には書けない)。lengthなど)に対して、自動的にIterateeに変換する持ち上げ(lift)を定義することができます。k)さらに、IterateeをMonadTransとMonadIOにしておくと便利です。liftIOモナディックiterateeの例をいくつか挙げておきます。ContMIteratee自体もモナドですので、もちろんもっと高レベルな書き方もできます。n)複数のiterateeを1つのenumeratorで並列に評価するというコンビネータを考えます。することにより、ファイル書き込みに簡単に進捗表示をつけることができるようになります。その他、時間のかかるような処理にプログレスバー表示を取り付けたりするのも簡単です。これは遅延I/Oでは(少なくとも低レベルの実装を隠したままでは)、できないことです。lengthといった、fold的な関数を見てきましたが、ではfilterやmapのような関数はどうでしょう?これらをうまく定義するために、"ネストしたenumerator"というものを導入します。これをenumerateeと呼びます。enumerateeは、ストリーム変換子としての型を持ちます。a)以下では簡単のために非モナド版iterateeで考えます。EOFouterストリームが元のstreamで、innerストリームがフィルタされたストリームになります。filterはまずpredicateを引数に取り、さらにもう一つ、フィルタされたinnerストリームを処理するためのiterateeを引数に取ります。mapIterも同様です。enumerateeを簡単に作るためにヘルパ関数を定義します。aouter要素をinner要素に変換するiterateeを引数に取り、enumerateeを構築します。これはmapIterと異なり、1対1の変換でなくても可能です。one-to-many、さらにはnone-to-manyの関係も有り得ます。これを利用すると、例えば、Word8のストリームからInt16のストリームに変換するenumerateeが定義できます。その他の実装ここまでiterateeの実装を一通り見てきましたが、実はiterateeの実装にはいろいろなものが提案されています。iteratee-0.2*10では上記実装とは少々異なったものになっています。}この定義では、Iterateeは(入力を何も消費しない場合でも)少なくとも一つのデータを要求します。その他の点には違いはありません(これは何が嬉しいんだろう?)。iteratee-0.4以降*11(未リリース?)ではRankNTypesを用いたCPSスタイルになっています。}CPSスタイルには、他の実装と異なり型が一つしかなくて関数なので、標準の合成演算子、>>=や(.)で合成できる、あるいは低レベルなiterateeの定義がシンプルになるなどのアドバンテージがあります。I/Oに匹敵するパフォーマンスが得られることでした。実際のところiterateeは速いのでしょうか?残念ながら上記の実装そのままではとても遅いです。それは主に文字の読み書きを1文字ずつやっていることが原因です。その解決策として、StreamGを1文字保持するのではなく、データのチャンクを保持するように変更します。するとI/Oをより大きなバッファで行うことができるようになります。することによりEmptyコンストラクタを消すことができます。データのチャンクを保持するバッファの選択がパフォーマンスに大きく影響を与えます。リストはHaskellにおいては一般的で自然な選択で、要素に対して多態でとても使いやすいのですが、パフォーマンスの点からはあまり良くありません。StrictなByteStringを用いると素晴らしいパフォーマンスが得られますが、一般性を損ないます。そこでiterateeパッケージではポリモーフィックなコンテナを表現できるListLikeクラス*12を利用しています。これによって、一般性を欠くことなく適切なコンテナを選択できるようになっています。まとめというわけでiterateeの概要を見てきました。この記事では基本的な部分しか説明できませんでしたが、実際のiterateeライブラリ*13をいじってみると理解が深まると思います。最近ではhackageDBに類似のライブラリ*14が登場したりしており、まだまだ発展途上のライブラリな感がありますが、将来的にはメインストリームになるんではないかなあと個人的には思っています。いうチームで参加していました。今回は問題がとても面白かったと思います。