Parsec 入門


Parsec で複合モナドを作る

print デバッグ

Parsec のパーサモナド ParsecT s u m a はモナドトランスフォーマーだ。これは、ParsecT モナドの中で、他のモナドの関数を使えるということを意味している。それに関してすぐに思いつくのは、IO モナドを使った print デバッグをパーサモナドで使えるかもしれないということだ。

複合モナドを作るといっても、モナドトランスフォーマーがあれば簡単だ。ParsecT を使ったパーサモナドの型指定のとき、ParsecT String () IO String の様に、ParsecT s u m a の m のところに IO を指定するだけでいい。

あとは、IO モナドの putStrLn 関数を ParsecT 複合モナドの中で使いたいときは、lift . putStrLn のように lift 関数と putStrLn 関数の合成関数をつくれば、lift . putStrLn 関数を ParsecT 複合モナドの関数として使うことができる。

Control.Monad.Trans モジュール

複合モナドについて概念を説明しても伝わらないと思うので実例をやってみる。

まず、ParsecT を複合モナドとして使うために、Text.Parsec モジュールに加えて、Control.Monad.Trans モジュールの lift 関数をインポートする。

ParsecT パーサモナドの複合モナドを使うためには lift 関数が必要だが、Text.Parsec モジュールをインポートしただけでは lift 関数はインポートされていないからだ。

Prelude> import Text.Parsec
Prelude Text.Parsec> import Control.Monad.Trans (lift)
Prelude Text.Parsec Control.Monad.Trans> :t lift
lift
:: (Monad m, Control.Monad.Trans.Class.MonadTrans t) =>
m a -> t m a

runPT 関数

もう一つの注意点は、ParsecT 複合モナドで作ったモナドプログラムを実行させるためには runPT 関数を使わなければならないということだ。一連の Parsec 関連の記事で用いた parseTest 関数は複合モナドには使えない。

準備ができたので、さっそく IO モナドの関数である putStrLn 関数を ParsecT 複合モナドの中で lift .putStrLn 関数として使ってみよう。

Prelude Text.Parsec Control.Monad.Trans> runPT ((lift . putStrLn) "hello")
 () "SourceName" "foo"
hello
Right ()

大成功。

入力バッファの内容を表示する

しかし、これだけでは能がないので、"hello, world" という文字列に string "hello" をマッチさせ、その時の入力バッファの内容の変化を putStrLn で表示してみよう。ParsecT モナドで入力バッファの内容を取り出す関数は getInput 関数だ。

Prelude Text.Parsec Control.Monad.Trans> :t getInput
getInput :: Monad m => ParsecT s u m s

実行結果は次のようになる。インプットバッファの文字列から string "hello" のマッチが行われたあとは "hello" が消費されて消えていることが分かる。

Prelude Text.Parsec Control.Monad.Trans> runPT (do x <- getInput; (lift .
 putStrLn) x; string "hello"; x <- getInput; (lift. putStrLn) x) ()
 "SourceName" "hello, world"
hello, world
, world
Right ()

分かりやすいように do 記法で書いたが >>= 演算子を活用するともっとコンパクトに書ける。

Prelude Text.Parsec Control.Monad.Trans> runPT (getInput >>= lift .
 putStrLn >> string "hello" >> getInput >>= lift . putStrLn) ()
 "SourceName" "hello, world"
hello, world
, world
Right ()

プログラムをファイルに記述するときは、きちんと型指定しないといけないのでやってみた。(行頭の .. はスペースに変える。)

Prelude Text.Parsec Control.Monad.Trans> :{
Prelude Text.Parsec Control.Monad.Trans| foo :: ParsecT String () IO ()
Prelude Text.Parsec Control.Monad.Trans| foo = do
Prelude Text.Parsec Control.Monad.Trans| .. x <- getInput
Prelude Text.Parsec Control.Monad.Trans| .. lift $ putStrLn x
Prelude Text.Parsec Control.Monad.Trans| .. string "hello"
Prelude Text.Parsec Control.Monad.Trans| .. x <- getInput
Prelude Text.Parsec Control.Monad.Trans| .. lift $ putStrLn x
Prelude Text.Parsec Control.Monad.Trans| :}

実行例は次のようになる。

Prelude Text.Parsec Control.Monad.Trans> runPT foo () "SourceName"
 "hello, world"
hello, world
, world
Right ()

複合モナドというと難しく感じるが、要するに ParsecT の型指定のときに ParsecT s u m a の m のところに IO を指定すれば、StateT 複合モナドの中で lift . putStrLn 関数が使えるようになると覚えれば簡単だ。こういうものは文章を読んで理解しようとするよりは手っ取り早くスニペットを作って実験してみたほうが分かりやすい。