これは、Scalaz 7 ライブラリを使用した iteratee の簡単な例で、関心のあるプロパティを示しています: 一定のメモリとスタックの使用。
問題
まず、各行に 10 進数の文字列を含む大きなテキスト ファイルがあり、少なくとも 20 個のゼロを含むすべての行を見つけたいとします。次のようなサンプル データを生成できます。
val w = new java.io.PrintWriter("numbers.txt")
val r = new scala.util.Random(0)
(1 to 1000000).foreach(_ =>
w.println((1 to 100).map(_ => r.nextInt(10)).mkString)
)
w.close()
という名前のファイルができましnumbers.txt
た。で開きましょうBufferedReader
:
val reader = new java.io.BufferedReader(new java.io.FileReader("numbers.txt"))
大きすぎるわけではありませんが (~97 メガバイト)、処理中にメモリ使用量が実際に一定に保たれているかどうかを簡単に確認するのに十分な大きさです。
列挙子の設定
まず、いくつかのインポートについて:
import scalaz._, Scalaz._, effect.IO, iteratee.{ Iteratee => I }
そして列挙子 (便宜上、IoExceptionOr
s をs に変更していることに注意してください):Option
val enum = I.enumReader(reader).map(_.toOption)
Scalaz 7 は現在、ファイルの行を列挙する適切な方法を提供していないため、一度に 1 文字ずつファイルをチャンクしています。もちろん、これは非常に遅くなりますが、ここではそれについて心配するつもりはありません。なぜなら、このデモの目的は、この大きなファイルを一定のメモリ内で、スタックを破壊することなく処理できることを示すことだからです。この回答の最後のセクションでは、パフォーマンスが向上したアプローチを提供しますが、ここでは改行で分割します。
val split = I.splitOn[Option[Char], List, IO](_.cata(_ != '\n', false))
そして、分割しないsplitOn
場所を指定する述語を取るという事実があなたを混乱させるなら、あなたは一人ではありません. 列挙型の最初の例です。先に進み、列挙子をラップします。split
val lines = split.run(enum).map(_.sequence.map(_.mkString))
これで、モナドにOption[String]
s の列挙子ができました。IO
列挙型でファイルをフィルタリングする
次は述語です。少なくとも 20 個のゼロを含む行が必要だと言ったことを思い出してください。
val pred = (_: String).count(_ == '0') >= 20
これをフィルタリング列挙型に変換し、列挙子をその中にラップできます。
val filtered = I.filter[Option[String], IO](_.cata(pred, true)).run(lines)
このフィルターを通過するすべてのものを出力するだけの単純なアクションを設定します。
val printAction = (I.putStrTo[Option[String]](System.out) &= filtered).run
もちろん、実際にはまだ何も読んでいません。これを行うには、次を使用しますunsafePerformIO
。
printAction.unsafePerformIO()
Some("0946943140969200621607610...")
これで、メモリ使用量が一定のままである間、s がゆっくりとスクロールするのを見ることができます。これは遅く、エラー処理と出力は少しぎこちないですが、9 行ほどのコードだと思いますが、それほど悪くはありません。
iteratee からの出力の取得
それはforeach
- っぽい使い方でした。折り畳みのように機能する iteratee を作成することもできます。たとえば、フィルターを通過した要素を集めてリストに返します。printAction
定義まで上記のすべてを繰り返し、代わりに次のように記述します。
val gatherAction = (I.consume[Option[String], IO, List] &= filtered).run
そのアクションを開始します。
val xs: Option[List[String]] = gatherAction.unsafePerformIO().sequence
コーヒーを飲みに行きましょう (かなり遠くにある必要があるかもしれません)。戻ってくると、None
(IOException
途中の の場合) またはSome
1,943 個の文字列のリストを含む があります。
ファイルを自動的に閉じる完全な (より高速な) 例
リーダーのクローズに関する質問に答えるために、上記の 2 番目のプログラムとほぼ同等の完全な動作例を次に示しますが、リーダーのオープンとクローズを担当する列挙子を使用します。また、文字ではなく行を読み取るため、はるかに高速です。まず、インポートといくつかのヘルパー メソッドについて:
import java.io.{ BufferedReader, File, FileReader }
import scalaz._, Scalaz._, effect._, iteratee.{ Iteratee => I, _ }
def tryIO[A, B](action: IO[B]) = I.iterateeT[A, IO, Either[Throwable, B]](
action.catchLeft.map(
r => I.sdone(r, r.fold(_ => I.eofInput, _ => I.emptyInput))
)
)
def enumBuffered(r: => BufferedReader) =
new EnumeratorT[Either[Throwable, String], IO] {
lazy val reader = r
def apply[A] = (s: StepT[Either[Throwable, String], IO, A]) => s.mapCont(
k =>
tryIO(IO(reader.readLine())).flatMap {
case Right(null) => s.pointI
case Right(line) => k(I.elInput(Right(line))) >>== apply[A]
case e => k(I.elInput(e))
}
)
}
そして今、列挙子:
def enumFile(f: File): EnumeratorT[Either[Throwable, String], IO] =
new EnumeratorT[Either[Throwable, String], IO] {
def apply[A] = (s: StepT[Either[Throwable, String], IO, A]) => s.mapCont(
k =>
tryIO(IO(new BufferedReader(new FileReader(f)))).flatMap {
case Right(reader) => I.iterateeT(
enumBuffered(reader).apply(s).value.ensuring(IO(reader.close()))
)
case Left(e) => k(I.elInput(Left(e)))
}
)
}
これで準備完了です:
val action = (
I.consume[Either[Throwable, String], IO, List] %=
I.filter(_.fold(_ => true, _.count(_ == '0') >= 20)) &=
enumFile(new File("numbers.txt"))
).run
これで、処理が完了するとリーダーが閉じられます。