Scalaの記事です。Haskellはあまり書けません。
Iterateeの複雑さから開放されたいのでPipe系ライブラリ使いましょうという記事です。
Iterateeとの比較や簡単な使い方についてつらつらと書いていきます。
Iterateeを複雑にしているものは何か?
ひとつは状態の多さであると考えられます。
IterateeはInputをとって、Iterateeを返すような手続きとみなせます。
この時、Inputには
- El
- Empty
- EOF
の三つの状態があり、場合分けしてそれぞれの処理を記述します。
さらに、Iteratee自体にも
- Cont
- Done
- Error
の三つの状態があり(Errorがない実装もある)、Iterateeを走査する時には場合分けをしなければなりません。
PlayのIterateeではパターンマッチやPartialFunctionで場合分けを記述しますが、ScalazのIterateeではcatamorphismによりそれぞれの処理を記述しています。
いずれにせよ、場合分けというものが煩雑になり易いということは変わりありません。
また、Iterateeの複雑さの権化であると考えられるのがEnumerateeです。
trait Enumeratee[From, To] {
def apply[A](inner: Iteratee[To, A]): Iteratee[From, Iteratee[To, A]]
}
これはIterateeを処理するIterateeであり、モナディックなEnumeratorでもあります。
Enumeratee一つ定義するだけでも、Inputを処理してIterateeを返す手続きをとってInputを処理してIterateeを返す手続きを返す手続きを定義する必要がある場合がほとんどなのでなかなかつらいです。
この記事で指すPipe系ライブラリというのは
- Haskell
- pipes
- conduit
- machines
- Scala
- scala-machines
- scalaz-stream
のことであり、Pipeモナドというのは構成要素として
- await -- 入力ストリームから値を取り出す
- yield -- 出力ストリームへ値を送る
を持つものとします。
今回参考にする実装はscalaz-streamですが、Pipeモナドの本質はどれも変わりありません。
val x = readLine
val y = readLine
println(x + y)
これは標準入力から2つ取り出して、連結したものを標準出力へ送っています。
Pipeはこの手続きを抽象化します。
import scalaz.stream._, Process._
val concat = for {
x <- await1[String]
y <- await1[String]
} yield x + y
入力列を与えてみましょう。
import scalaz._, Scalaz._
(Process("foo", "bar", "baz") |> concat toList) assert_=== List("foobar")
これではあまり面白くないですね。
実際にIOを利用しましょう。
(io.linesR("build.sbt") |>
concat |>
process1.utf8Encode to
io.chunkW(System.out)).run
.run
- ファイルの読み込み
- 二行読み込んで連結
- バイト列へ変換
- 標準出力へ出力
といったことをしています。
Pipe系ライブラリの大きな特徴は入出力手続きをモナドにより組み立てられるということです。
先のconcat
の例では、
入力を2つとって連結し、出力する
ということを抽象化しており、入力元と出力先は自由に決めることができます。
さらに、この手続きは合成することが可能です。
IOを使った例ではconcat
した後にprocess1.utf8Encode
で文字列をバイト列に変換しています。
このようにPipe系ライブラリでは入出力手続きをパイプのように連結していくことができます。