FSharpxのIterateeで遊ぶ

最近Haskellを勉強してるぺんぎんです。
「F#全然やってないじゃない」と言われそうですが、決して浮気しているわけではありませんよ?

FSharpxでIteratee

ところで、Haskellの記事を漁っているとIterateeの話がでてきたりする。

そしてF#では、FSharpxというOSSライブラリでその存在が確認できる。
なんというか、よくこんなライブラリをつくったなーとか思わないでもないが、まぁあるなら便利なこともあるので使わせていただくとしましょう。

今回はとりあえずで使ってみるだけなので、下記サイトと同様のものを作れたら満足かなということにしました。

使ってみよう Enumerator - あどけない話

おまじない

#nowarn "40"

open FSharpx
open FSharpx.ByteString
open FSharpx.Iteratee
open FSharpx.Iteratee.Binary

あまり警告はでないように書きたいところですが、ちょっと邪魔だったので今回は#nowarnをつけておきます。

注意点として、後で出てきますが、Listに対してSeq.foldしているところがあります。
最初はList.foldを使おうと思っていたのですが、何も考えずにFSharpx.Iterateeをインポートした影響でIteratee.List.foldとぶつかってしまったのでした・・・。
moduleに別名をつければ回避できるのですが修正が面倒くさかったのでごめんなさい。

Iterateeをつくる?

FSharpx側でコンピュテーション式が定義されているから、Haskellコードのイメージそのままですぜ!

let rec consumer =
  iteratee {
    let! mw = head
    match mw with
    | None -> ()
    | Some w ->
      printf "XXX "
      w |> (ByteString.singleton >> ByteString.toString) |> printfn "%s"
      return! consumer // #nowarnを使わなかったら警告がでるよ
  }

とりあえず走らせてみる。

> run <| consumer

反応がない。そりゃそうですよねー。

Enumeratorをつくる?

さて、元記事(というかHaskell)のほうではenumListという非常にうらやましいものがあるらしいのだが、FSharpxには残念ながら存在しない。
ないなら作れば・・・とも思ったが時間が空いたときにでも作ろうということで今回は全力で怠惰に。

let listFeeder : Enumerator<ByteString, unit> =
  ["12"; "34"]
  |> List.map ByteString.ofString
  |> Seq.fold ByteString.append ByteString.empty
  |> enumerate

文字列をByteStringに変換してくっつけてからEnumeratorを練成してみた。
わかりやすさ重視のため、ここだけ先行して型を記述しておく。

> run <| listFeeder consumer
XXX 1
XXX 2
XXX 3
XXX 4

わーい。

入力をふやす?

ファイルからの入力だって。
ほんとは例外処理をきちんとしないといけないのだけど、今回はちょっとさぼり。。。

let fileFeeder : Enumerator<ByteString, unit> =
  IO.readFile @".\FILE"
  |> Seq.map ByteString.ofString
  |> Seq.fold ByteString.append ByteString.empty
  |> enumerate

ちなみに、IO.reaaFileもFSharpxで提供されている関数です。

> run <| (fileFeeder << listFeeder) consumer
XXX 1
XXX 2
XXX 3
XXX 4
XXX 5
XXX 6
XXX 7
XXX 8

ちゃんと合成できていますね。

仕事をふやす?

別のお仕事もしようぜ!

let rec consumer2 =
  iteratee {
    let! mw = head
    match mw with
    | None -> ()
    | Some w ->
      printf "YYY "
      printfn "%s" <| (ByteString.singleton >> ByteString.toString) w
  }

YYYの出力に変えて、再帰を除去。
で、こいつを合成すると・・・

> run <| (fileFeeder << listFeeder) (consumer2 >>. consumer)
YYY 1
XXX 2
XXX 3
XXX 4
XXX 5
XXX 6
XXX 7
XXX 8

あぷりかてぃぶふぁんくたー!

でもこの (<*) の使い方は正しいのだろうか・・・・要検証。
(8/17追記: (<*)関数ではなく(>>.)関数を使えばよかった。ここでの(>>.)関数はHaskellでいうところの(>>)関数である。)

仲介者をつかう!?

さて、元記事ではisolate関数を使っているが、これまたFSharpxさんにはないのですよー。

怠惰にいきたいけどないとどうしようもなさそうなので、今回はHaskellでの実装をパク・・・見よう見まねでF#に移植してみた。
ついでに結合用のjoinI関数も作ってみる。

// val isolate : int -> Enumeratee<ByteString,ByteString,'a>
let rec isolate n = function
| step when n <= 0 -> returnI step
| Continue k ->
  let rec loop = function
  | Empty -> Continue loop
  | Chunk str when ByteString.isEmpty str -> Continue loop
  | Chunk str ->
    let len = ByteString.length str
    if len < n then isolate (n - len) (k (Chunk str))
    else let s1, s2 = ByteString.splitAt n str in Done(k (Chunk s1), (Chunk s2))
  | EOF -> Done(k EOF, EOF)
  Continue loop
| step -> (drop n) *> (returnI step)

let joinI outher =
  let rec check = function
  |  Continue k ->
    (fun s ->
      match s with
      | Continue _ -> raise <| exn "joinI: divergent iteratee"
      | _ -> check s
    ) (k EOF)
  | Done (x, _) -> returnI x
  | Error e -> raise e
  outher >>= check

一番上のコメントはisolateのシグネチャです。
シグネチャファイルちゃんと生成しろよという話ですね、、、

> run <| listFeeder (joinI (isolate 2 consumer))
XXX 1
XXX 2

最初は「おいおい、ほんとに動いたよ・・・」みたいな感じでしたが、とりあえず動きます。

「これもう少しきちんと書いたらpull requestだせるかな?」

と、そんな妄想をしながらFSharpxのソースコードリーディングを続けるぺんぎんさんでしたとさ。

詳細な解説

あとでかく