Inside Parsec


Text.Parsec.Prim: State s u

Text.Parsec.Prim

今回の記事からは Text.Parsec.Prim モジュールを探索する。Prim は primitive (原始的) の意味で Haskell の心臓部の関数が記述されている。

unknownError

Hackage の Text.Parsec.Prim の文書の先頭は unknownError :: State s u -> ParsError 関数だ。戻り値が ParseError だから、例外を生成する関数のようだが、引数の State s u とは一体何なのだろうか。これをきっかけに探索してみる。

State s u

parsec.ghci で ghci を設定する

まずは、ghci で Text.Parsec プログラムを探索するための前準備。前回作成した parsec.ghci 設定ファイルで ghci を設定する。サブモジュールをすべてインポートするので、上記の State 型の詳細を :t コマンドで表示できるようになる。

Prelude> :script parsec.ghci
Parsec>

State s u とはどんなデータ型か

そこで State s u とはどんなデータ型なのかを調べてみる。

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

これを見ると、State s u 型とは、stateInput フィールドと、statePos フィールドと、stateUser フィールドの3つからなっていることが分かる。また、型変数 s, u が使われているので stateInput フィールドの値と、stateUser フィールドの値は多形性がある。

stateInput フィールドには入力文字列が納められ、statePos フィールドには SourcePos 型のソース位置が納められ、stateUser にはユーザ状態の値が納められる。

getParserState

問題はどうやって State 型のデータを取得するかだが、幸いなことに getParserState というパーサがある。これはパーサ状態 State s u を取り出して返す。

getParserState :: Monad m => ParsecT s u m (State s u)

Returns the full parser state as a State record.

getParserState はパーサ・モナドなので runPraser 関数を利用することができる。

Parsec> (Right ps) = runParser getParserState () "SourceName" "hello"
Parsec> :t ps
ps :: State [Char] ()

このときの runParser 関数の戻り値は Either 型なので、パターンマッチの (Right ps) で ps には State s u 型のパーサ状態が取り出せる。また、このとき ghci のコマンドの :t ps で ps の型を表示できる。

State 型のフィールド

State 型はパーサ状態と呼ばれている。上で取得した State 型 ps の内容は、フィールド名のアクセサで取り出す。

Parsec> stateInput ps "hello" Parsec> statePos ps "SourceName" (line 1, column 1) Parsec> stateUser ps ()

これは runParser の引数に対応しているように見える。stateInput フィールドには入力の文字列が納められ、statePos フィールドにはソース位置が SourcePos 型で納められ、stateUser フィールドにはユーザの状態が納められている。ちなみに、runParser の型は次のようになる。

Parsec> :i runParser
runParser ::
Stream s Data.Functor.Identity.Identity t =>
Parsec s u a -> u -> SourceName -> s -> Either ParseError a
-- Defined in ‘Text.Parsec.Prim’

データ型 State s u に納められているのは入力の文字列、キャラクタポインタ、ユーザ状態という Parsec の中心となるデータを集めたものだった。

パーサのマッチによる State の状態変化

そこで、runParser でパーサのマッチを行ったとき State の状態がどう変わるかを見てみた。

Parsec> runParser (string "foo" *> getParserState >>= \x -> return
 (stateInput x, statePos x, stateUser x)) "hello" "SourceName" "foobar"
Right ("bar","SourceName" (line 1, column 4),"hello")

最初の入力で与えた文字列 "foobar" からパーサ string "foo" にマッチした "foo" が消費され、ソース位置が (line 1, column 4) に移っている。また、ソース名 "SourceName" とユーザ状態の "hello" も取り込まれている。このことから、パーサの作業が行われる場所が State s u 型であることが分かる。

State データ型を作成する

State データ型を直接的に作成できないかを調べてみたが、State データコンストラクタでできそうだ。

Parsec> :t State
State :: s -> SourcePos -> u -> State s u

そこで newState を作ってみた。

Parsec> newState = State "hello" (newPos "SourceName" 1 1) "foo"
Parsec> stateInput newState
"hello"
Parsec> statePos newState
"SourceName" (line 1, column 1)
Parsec> stateUser newState
"foo"

この newState に unknownError 関数を関数適用してみたら、ParseError 型の値が得られた。

Parsec> unknownError newState
"SourceName" (line 1, column 1):unknown parse error

このように、謎の State s u 型は、Parsec の作業の中心部分のデータを持つデータ型だった。

Text.Parsec.Prim の部品の取り出し

Text.Parsec.Prim のソースコードをいきなり読むのは不可能だが、こういう風に部品をとりだしてテストできるのはありがたい。こういうことができるのも Haskell の関数型プログラム言語という特徴からではないだろうか。

runParsecT

State s u がなぜパーサ状態なのだろうかと不思議に思っていたのでいろいろ調べてみたら、ParsecT モナドの内部的な unpacker である runParsecT を見つけた。runParsecT の型は次のようになる。

Parsec> :t runParsecT
runParsecT
:: Monad m =>
ParsecT s u m a -> State s u -> m (Consumed (m (Reply s u a)))

これは runParsecT p s のように、runParsecT がパーサ p と状態 s を引数にとることを示している。この形式は State モナドの、runState p s の形式と同じだ。つまり、ParsecT モナドは State モナドの一種であるようだ。この後の解読も State モナドとの類推ができるかもしれない。

Parsec> :t runParsecT getInput (State "hello" (newPos "SourceName" 1 1) ())
runParsecT getInput (State "hello" (newPos "SourceName" 1 1) ())
:: Monad m => m (Consumed (m (Reply [Char] () [Char])))

runParserT は State の引数は取らないが、引数を元に内部的に State s u 型を作り、runParsecT に渡しているようだ。

Parsec> runParserT getInput () "SourceName" "hello"
Right "hello"

State モナドと runState 関数

State モナドの場合、昔の定義では、コンテナの中は runState フィールド名つきの関数だ。

data State s a = State { runState :: s -> (a, s) }

したがって、Stateモナド sm にアクセサ runState を適用すると s -> (a, s) 型の関数が現れるので、runState sm s でモナド sm のプログラムを状態 s のもとで実行することができる。

runParsecT p s はパーサモナド p のプログラムをパーサ状態 s のもとで実行する。runState sm s と同じ発想のように見える。