Inside Parsec


Text.Parsec.Prim 攻略用部品

設定ファイル

Text.Parsec.Prim 攻略用の部品を作って ghci 設定用のファイル parsec.ghci に追加した。

Text.Parsec.Prim には数多くの関数があるが、それらは Parsec で定義された特定のデータ型を引数に取るものが多い。それらの関数の機能をテストするためには、予め定義された特定のデータ型の値が必要になってくる。それらを ghci 設定用のファイルに追加したのだ。

ファイルの内容は次のようになる。

{- parsec.ghci: ghci configulation for Text.Parsec -}
import Text.Parsec.Pos
import Text.Parsec.Error
import Text.Parsec.Prim
import Text.Parsec.Char
import Text.Parsec.Combinator
:set prompt "Parsec> "
s = "input stream"
u = "user state"
pos = newPos "SourceName" 1 1
st = State s pos u
msg = "hello, parsec"

これを :script parsec.ghci で ghci に取り込むと、

s: 入力文字列
u: ユーザ用状態
pos: Pos 型の位置情報
st: State 型のパーサ状態
msg: エラーメッセージ

を利用できるようになる。これらを使うと Text.Parsec.Prim のいろいろな関数の動作をテストできる。例えば、unknownError の動作は st を引数として与えることで確認できる。

Prelude> :script parsec.ghci
Parsec> :t unknownError
unknownError :: State s u -> ParseError
Parsec> unknownError st
"SourceName" (line 1, column 1):unknown parse error
つまり unkownError 関数はパース状態を引数にとり、ParserError を返すことが分かる。以下の実験は Text.Parsec.Prim の Haddoc 文書の初めの方に現れた関数の動作をこれらの値を用いてテストしている。

unknownError 関数

unkownError 関数は Text.Parsec.Prim の冒頭に定義されている関数だ。unknownError の戻り値は PraseError 型だ。引数はバーサ状態 State s u を取るので、上で定義した st を使ってみる。ParseError は、Show モナドのインスタンスなのでそのまま表示できる。

Parsec> :t unknownError
unknownError :: State s u -> ParseError
Parsec> unknownError st
"SourceName" (line 1, column 1):unknown parse error

動作を確認できたので、Hackage のページの source をクリックしてソースを表示してみた。

unknownError :: State s u -> ParseError
unknownError state = newErrorUnknown (statePos state)

unknownError 関数はパーサ状態 state を引数としてとるが、内部では、state のフィールドから statePos アクセサで state のソース位置 SourcePos を取り出して、newErrorUnknown 関数に渡している。

newErrorUnknown 関数

そこで、このソースの newErrorUnknown 関数もテストしてみる。

Parsec> newErrorUnknown (statePos st)
"SourceName" (line 1, column 1):unknown parse error

newErrorUnknown 関数はソース位置 SourcePos を引数にとり、ParseError 型の関数を戻している。

newErrorUnknown 関数の定義は Text.Parsec.Error モジュールにあり次のようになっている。

newErrorUnknown pos
   = ParseError pos []

位置情報のみでエラーメッセージのない ParseError が返されている。残念ながら ParseError データコンストラクタはエクスポートされていないので実験できない。しかし、unknownError のソースの意味が分かる。unknownError関数の目的は、ソース位置情報のみでエラーメッセージのない ParseError を作成することだ。

sysUnExpectError

次に sysUnExpectError 関数だがソースは次のようになる。

sysUnExpectError :: String -> SourcePos -> Reply s u a
sysUnExpectError msg pos = Error (newErrorMessage (SysUnExpect msg) pos)

これは、引数の文字列を使って SysUnExpcet msg メッセージを作りそれと SourcePos から ParseError を作り、それを Error データ型で包む。作成された値は Reply s u a 型になる。

これは、次のように実験ができた。

Parsec> :t Error (newErrorMessage (SysUnExpect msg) pos)
Error (newErrorMessage (SysUnExpect msg) pos) :: Reply s u a

unexpected 関数

最後に unexpected 関数だが、これは戻り値が ParsecT s u m a だからパーサを作る関数だ。もちろん show 関数は使えないので、parseTest 関数の引数にして動作を確認する。

unexpected :: Stream s m t => String -> ParsecT s u m a
Parsec> parseTest (unexpected msg) "foo"
parse error at (line 1, column 1):
unexpected hello, parsec

unexpected 関数のソースは次のようになる。戻り値が ParsecT s u m a パーサなので、パーサがどういう風に作られるかということの見本になっている。

unexpected :: (Stream s m t) => String -> ParsecT s u m a
unexpected msg
   = ParsecT $ \s _ _ _ eerr ->
     eerr $ newErrorMessage (UnExpect msg) (statePos s)

unexpected 関数は文字列 msg を引数にとる。msg は UnExpect コンストラクタで包まれて UnExpect メッセージになる、このメッセージと SourcePos s から newErrorMessage で ParseError を作り、継続(関数)の eerr に渡される。

eerr は runParsecT で実関数が与えられているが、ParseError を m(Consumed (m (Reply s u a))) 型にラッピングして unexpected 関数の戻り値としている。

また、これは、ParsecTパーサ の unParser フィールドの関数 \s cok cerr eok eerr -> ... の引数のうち状態 s と第5引数の eerr だけを使って関数の本体を記述していることを示している。

cok cerr eok eerr の各引数に与えられる関数は runParsecT が割り当てる。

runParsecT :: Monad m => ParsecT s u m a -> State s u -> m (Consumed (m
 (Reply s u a)))
runParsecT p s = unParser p s cok cerr eok eerr
   where cok a s' err = return . Consumed . return $ Ok a s' err
     cerr err = return . Consumed . return $ Error err
     eok a s' err = return . Empty . return $ Ok a s' err
     eerr err = return . Empty . return $ Error err

上のソースのうち eerr に与えられる値はテストすることができる。

Parsec> newErrorMessage (UnExpect msg) (statePos st)
"SourceName" (line 1, column 1):
unexpected hello, parsec

ParseError を発生させるだけの機能しかないパーサ (unexpected "foo") は、構造が簡単なのでその意味が分かりやすい。無名関数 \s cok cerr eok eerr -> ... の本体部分の記述がそのパーサの動作の本質だということだ。また、cok cerr eok eerr などは戻り値をモナドに包むラッパー関数であることが分かる。

Text.Parsec.Prim の関数は、引数に渡すデータ型の値を作ることで、その動作をテストできる。テストしながらソースコードを読むとその意味がよく分かる。