あどけない話

Internet technologies

使ってみよう Enumerator

Enumerator Package - Yet Another Iteratee Tutorialは、Iteratee: 列挙ベースのI/Oよりは分かりやすいのですが、やっぱりよく分かりません。なぜなら、僕は使い方を知りたいのに、作り方が書いてあるからです。そこで、Enumerator ライブラリの使い方を簡単に紹介します。

登場人物

  • Iteratee
    • 入力をもらって計算をします
    • run_ で実行します
    • IO モナドが指定されていれば、副作用を起こせます
    • オートマトンと考えると分かりやすいです
    • Iteratee 同士は (>>=) で合成できます
      • Iteratee >>= Iteratee → Iteratee
  • Enumerator
    • Iteratee と ($$) で合成することにより、新たな Iteratee になります
      • Enumerator $$ Iteratee → Iteratee
    • 入力を与えてオートマトンの状態を進めます
    • Enumerator 同士は (<==<) で合成できます
      • Enumerator <==< Enumerator → Enumerator
  • Enumeratee
    • Enumerator と Iteratee の間に入って、入力を加工します
    • Iterator と =$ で合成すると Iteratee になります
      • Enumeratee =$ Iteratee → Iteratee
    • Enumerator と $= で合成すると Enumerator になります。
      • Enumerator $= Enumeratee -> Enumerator

おまじない

以下のようなモジュールが import されていると仮定して、話を進めます。

{-# LANGUAGE OverloadedStrings #-}

import Control.Monad.IO.Class (liftIO)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as B -- for OverloadedStrings
import Data.Enumerator (($$), (<==<), ($=), (=$))
import qualified Data.Enumerator as E
import Data.Enumerator.Binary as EB
import qualified Data.Enumerator.List as EL
import Data.Maybe

Iteratee を作る。

まず、Iteratee を作ってみましょう。それには、小さな Iteratee を利用するのが一番です。ここでは、EB.head を使いましょう。

EB.head :: Monad m => Iteratee ByteString m (Maybe Word8)

このように、EB.head は Maybe Word8 を返します。m に IO を指定すれば、副作用を起こせるので、与えられた Word8 を標準入力に出力してみましょう。

consumer :: E.Iteratee BS.ByteString IO ()
consumer = do
    mw <- EB.head
    case mw of
        Nothing -> return ()
        Just w  -> do
            liftIO . putStr $ "XXX "
            liftIO . BS.putStrLn . BS.singleton $ w
            consumer

実際に走らせてみます。

> E.run_ consumer

何も起きません! 当然です。何も入力を与えてないからです。

Enumerator を作る

入力を与えるために Enumerator を作りましょう。

Enumerator のウリは、入力源の抽象化です。ファイル、ハンドル、ソケット、リストなどを同じように、しかも継ぎ目なく扱えます。ここでは、リストから入力を生成する E.enumList を利用しましょう。

E.enumList :: Monad m => Integer -> [a] -> Enumerator a m b

とりあえず、第一引数のことは深く考えないんで下さい。以下のように ByteString のリストを入力源とした Enumerator を作ります。

listFeeder :: E.Enumerator BS.ByteString IO a
listFeeder = E.enumList 1 [ "12", "34" ]

Enumerator と Iteratee を ($$) で合成して新たな Iteratee とし、E.run_ で走らせてみます。

> E.run_ $ listFeeder $$ consumer
XXX 1
XXX 2
XXX 3
XXX 4

うまくいきました。パチ、パチ、パチ。

入力を増やす

リストの入力の後に、さらにファイルから入力を与えてみましょう。ファイルを入力源として Enumerator を作るのは EB.enumFile です。

EB.enumFile :: FilePath -> Enumerator ByteString IO b

今、"FILE" に "5678" という文字列が格納されているとしましょう。これを読み込む fileFeeder を以下のように定義します。

fileFeeder :: E.Enumerator BS.ByteString IO a
fileFeeder = EB.enumFile "FILE"

listFeeder と fileFeeder の両方を指定して、実行してみましょう。

> E.run_ $ fileFeeder $$ listFeeder $$ consumer
XXX 1
XXX 2
XXX 3
XXX 4
XXX 5
XXX 6
XXX 7
XXX 8

Enumerator ライブラリが便利そうに思えてきましたね。

上の例では $$ は右結合であるので、Iteratee と Enumerator を合成しています。一方、以下は Enumerator 同士を合成して、Enumerator とする例です。

> E.run_ $ (fileFeeder <==< listFeeder) $$ consumer
上に同じ

仕事を増やす

Iteratee は、ちゃんと食べ残しを次の人に渡します。ですので、Iteratee の後に、さらに別の Iteratee が仕事をすることもできます。上記の consumer は、入力をすべて食べてしまいますので、他の人に残す余地がありません。そこで、食べ残しをする consumer2 を定義していましょう。

consumer2 :: E.Iteratee BS.ByteString IO ()
consumer2 = do
    mw <- EB.head
    case mw of
        Nothing -> return ()
        Just w  -> do
            liftIO . putStr $ "YYY "
            liftIO . BS.putStrLn . BS.singleton $ w

目印の文字列が XXX から YYY に変わったこと、再帰をしないことに注意してみて下さい。Iteratee 同士は (>>=) で合成できるので、以下のように実行できます。

> E.run_ $ fileFeeder $$ listFeeder $$ (consumer2 >> consumer)
YYY 1
XXX 2
XXX 3
XXX 4
XXX 5
XXX 6
XXX 7
XXX 8

仲介者を使う

最後に Enumeratee を使ってみましょう。一番簡単な Enumeratee は、EB.isolate です。これは、与えられた数の個数だけ入力を取り出してくれます。

EB.isolate :: Monad m => Integer -> Enumeratee ByteString ByteString m b

使ってみましょう。

> E.run_ $ listFeeder $$ EB.isolate 2 =$ consumer
XXX 1
XXX 2

出力が4行から2行になりました。

Enumeratee は、Enumerator と合成することもできます。

E.run_ $ (listFeeder $= EB.isolate 2) $$ consumer
XXX 1
XXX 2

それぞれのモジュール

Data.Enumerator.Binary は、入力の ByteString をバッファ単位で管理します。EB.head で取り出すと、一文字ずつ、つまり Word8 が取り出せます。

Data.Enumerator.Text は、入力の Text を行単位で管理します。ET.head で取り出すと、一文字ずつ、つまり Char が取り出せます。

もし、EL.head で取り出すと、それぞれバッファや行、すなわち ByteString や Text が出てきます。素直に、Chunk [a] の a を取り出すだけですね。