Inside Parsec


Text.Parsec.Prim: mkPT, tokenPrim

mtPT 関数

mkPT 関数のソース

Text.Parsec.Prim モジュールで定義されている mkPT 関数はパーサモナドを作るための雛形だ。ソースは次のようになる。
-- | Low-level creation of the ParsecT type. You really shouldn't have
 to do this.
mkPT :: Monad m => (State s u -> m (Consumed (m (Reply s u a)))) ->
 ParsecT s u m a
mkPT k = ParsecT $ \s cok cerr eok eerr -> do
           cons <- k s
           case cons of
             Consumed mrep -> do
                       rep <- mrep
                       case rep of
                         Ok x s' err -> cok x s' err
                         Error err -> cerr err
             Empty mrep -> do
                       rep <- mrep
                       case rep of
                         Ok x s' err -> eok x s' err
                         Error err -> eerr err

mkPT を使ってパーサを作ろうと考えてはいけないと警告されているが。これがパーサモナドの原型であるのは間違いないので、解読してみる。

mkPT 関数の型

まず、関数の型だが mkPT k と言うように引数 k を1個だけ使って、戻り値は PrasecT s u m a 型のモナド値である。

引数 k の型

引数の k は関数で次のような型をしている。

State s u -> m (Consumed (m (Reply s u a)))

パーサ状態 State s u を引数とし m (Consumed (m (Reply s u a))) が戻り値である。State s u のフィールドは次のようになる。

data State s u
  = State {stateInput :: s, statePos :: !SourcePos, stateUser :: !u}
  	-- Defined in ‘Text.Parsec.Prim’

stateInput には入力文字列、statePos には SourcePos、stateUser にはユーザ状態が納められている。戻り値は複雑に入れ子になった ParsecT モナドトランスフォーマの内部モナドだが、その中に入力文字列が消費されたかどうか Consumed という情報と、マッチの成否を伝える Reply 情報が納められている。

関数 k はパーサ状態 s からパースの解析結果を Consumed と Reply の情報を内部モナドにラッピングして返す関数だ。

mkPT 関数の機能

mkPT k は上の関数 k を引数にとり \s cok cerr eok ecrr -> do {...} という無名関数を ParsecT データ型の (unParser) フィールドに収めるのが仕事である。k は State s u を引数にとり m (Consumed (m (Reply s u a))) モナド型のデータを返す関数なので、\s cok cerr eok ecrr -> do {...} の様に無名関数の本体は do 記法でプログラムすることができる。

cons データの取り出し

プログラムの中心部分は cons <- k s で関数 k はパーサ状態 s に対してパースを行い、 m (Consumed (m (Reply s u a))) 型のデータを返す。

cons は <- 記法で (k s) から値を返されているので、モナドがアンラップされて Consumed (m (Reply s u a)) 型になっている。<- 記法でモナドの皮を剥いたら、その中からConsumed 型が現れる。

case cons of による分岐

case cons of のパターンマッチはこの Consumed (m (Reply s u a)) 型に対して行われ、Consumed mrep と、Empty mrep に分岐する。

分岐後は rep <- mrep でモナドのアンラップが行われ、case rep of によって、Ok x s' err と Err err に分別される。

結局のところ mkPT 関数は (k s) の戻り値である m (Consumed (m (Reply s u s))) 型のデータから cok cerr eok eerr の継続に渡すデータを分別するジェネリックな関数だ。

tokenPrim 関数

mkPT 関数は概念的なもので、実際には使われないようだ。実際のパーサを作るのに利用されるのは tokenPrim という別の関数だ。tokenPrim の型は次のようになる。

tokenPrim
  :: Stream s m t =>
     (t -> String)          showTok
     -> (SourcePos -> t -> s -> SourcePos)          posFromTok
     -> (t -> Maybe a)          testTok
     -> ParsecT s u m a

tokenPrim 関数の引数は全て関数で、引数の showTok はトークンを文字列化する関数、posFromTok はマッチを行った後のソース位置を計算する関数、testTok はマッチが成功したかどうかを判別する関数( Maybe 型を返す)だ。

Hackage には tokenPrim の用例が出ていたので実験してみた。コードが長いのでファイル mychar.hs に作成した。

import Text.Parsec
import Text.Parsec.Pos
import Data.Functor.Identity
mychar :: Char -> ParsecT String () Identity Char
mychar c
  = tokenPrim showChar nextPos testChar
  where
    showChar x = "'" ++ [x] ++ "'"
    testChar x = if x == c then Just x else Nothing
    nextPos pos x xs = updatePosChar pos x

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

Prelude> :l mychar.hs
[1 of 1] Compiling Main ( mychar.hs, interpreted )
Ok, modules loaded: Main.
*Main> parseTest (mychar 'a') "abc"
'a'
*Main> parseTest (mychar 'b') "abc"
parse error at (line 1, column 1):
unexpected 'a'

なんとか tokenPrim でパーサを作ることができるようになったので、Parsec に装備されているパーサの解読ができそうだ。