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 に装備されているパーサの解読ができそうだ。