使ってみよう Enumerator
Enumerator Package - Yet Another Iteratee Tutorialは、Iteratee: 列挙ベースのI/Oよりは分かりやすいのですが、やっぱりよく分かりません。なぜなら、僕は使い方を知りたいのに、作り方が書いてあるからです。そこで、Enumerator ライブラリの使い方を簡単に紹介します。
登場人物
- Iteratee
- Enumerator
- Iteratee と ($$) で合成することにより、新たな Iteratee になります
- Enumerator $$ Iteratee → Iteratee
- 入力を与えてオートマトンの状態を進めます
- Enumerator 同士は (<==<) で合成できます
- Enumerator <==< Enumerator → Enumerator
- Iteratee と ($$) で合成することにより、新たな Iteratee になります
- 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 を取り出すだけですね。