Inside Parsec


Text.Parsec.Prim: tokenPrim

Text.Parsec.Prim モジュールで定義されている tokenPrim 関数は自前のパーサを作るための関数だ。

tokenPrim 関数の型は次のようになっている。

tokenPrim :: (Stream s m t)
          => (t -> String)                      
          -> (SourcePos -> t -> s -> SourcePos) 
          -> (t -> Maybe a)                     

tokenPrim 関数の引数はすべて関数だ。第1引数は、トークンを文字列に変換する関数。第2引数は次のソース位置を計算する関数。第3引数は入力の先頭の文字がパーサのパターンにマッチするかどうかの関数だ。使い方は、次の実例を見れば分かる。

char 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

上のソースで updatePosChar という関数があるが、これは Text.Parsec.Pos で定義されている関数で、パーサが1文字消費したときに次のキャラクタポインタを返す関数だ。

そこで、tokenPrim のソースを読んでみた。

tokenPrim showToken nextpos test = tokenPrimEx showToken nextpos Nothing test
tokenPrimEx showToken nextpos Nothing test
  = ParsecT $ \(State input pos user) cok _cerr _eok eerr -> do
      r <- uncons input
      case r of
        Nothing -> eerr $ unexpectError "" pos
        Just (c,cs)
         -> case test c of
              Just x -> let newpos = nextpos pos c cs
                            newstate = State cs newpos user
                        in seq newpos $ seq newstate $
                           cok x newstate (newErrorUnknown newpos)
              Nothing -> eerr $ unexpectError (showToken c) pos

tokenPrim 関数は tokenPrimEx 関数の別名だ。引数が tokenPrimEx のほうが一つ多いが tokenPrim 関数では Nothing に束縛されている。

tokenPrimEx 関数は ParsecT s u m a モナドのフィールドに \s cok cerr eok eerr -> . 型の無名関数を収める仕事をしている。\s eok _cerr _eok eerr の様に引数の cerr と eerr にアンダーバーがついているのは、プログラムの中でこれらの引数を使わないからだ。すなわち、tokenPrim で作成されるパーサはパターンがマッチしたときには文字の消費が起きるが、マッチしないときは消費が起きないことを示している。

さて、無名関数の本体の定義であるが、最初に do キーワードがあるのでこれは do 記法によるモナドのプログラムだ。何のモナドかというと m (Consumed (m (Reply s u a))) モナドを戻り値として返すプログラムだ。do 記法の最初の行の r <- uncons input だが、uncons は Text.Parsec.Prim に定義されている関数で、入力の先頭の文字と、その後の文字列のペアを m (Maybe (t, s)) にして返す。uncons input の値は <- でモナドのアンラップがおきて r にはモナドのない Maby (t, s) が束縛される。

次に case r of では Nothing と Just (c, s) のパターンで分岐がおきる。uncons が先頭の文字列の取得に失敗した場合 Nothing になり、成功した場合には Jus (c, cs) である。

r が Nothing のときは、eerr 関数で unExpectedError "" pos で作られたメッセージを返す。

r が Just (c, cs) のときは、文字 c について case test c of でパターンマッチで分岐する。test 関数はユーザがパターンとして与えた関数で test c が成立するときは Just x を返し、パターンマッチが失敗したら Nothing を返す。

パターンマッチが成功しなかったときを先にみると、eerr 関数で unExpectedError (showTok c) pos で作ったメッセージを返す。

パターンマッチが成功した場合は c == x だ。この場合、

seq newpos $ seq newstate $ cok x newstate (newErrorUnknown newpos)

なので seq 関数によって newpos と newstate を正格評価(先に評価を確定させて)して cok x newstate (newErrorUnknown newpos) を返す。要するにパターンマッチ時の戻り値 x と新しい State と ParseError を cok 関数でモナドにラッピングして返す。

このことから、tokenPrim 関数がパターンマッチが成功したら文字列を消費して成功の戻り値を返すが、パターンマッチが失敗したら文字の消費はせず、エラー情報のみを返すパーサを作ることが分かる。

uncons の値が m (Maybe (t, s)) 型である理由は、パーサのプログラムを do 記法で記述するためだったようだ。パーサのプログラムをモナド型の do 記法で記述できることの利点はわからないが、重要な意図があるのだろう。ここでは、単に tokenPrimEx のプログラムがモナドプログラムであると知ったことに満足しよう。