Parsec 入門


Parsec makeTokenParser を使う。

Parsecで実際にパーサを作るときは、Text.Parsec.Token モジュールの makeTokenParser を使ってトークンパーサを自動作成したほうがいい。プログラム言語のトークンは共通しているものが多いし、makeTokenParser を使って作成したトークンは空白処理など行き届いた動作をするからだ。
Prelude> import Text.Parsec.Token
Prelude Text.Parsec.Token> :t makeTokenParser
makeTokenParser
:: Text.Parsec.Prim.Stream s m Char =>
GenLanguageDef s u m -> GenTokenParser s u m

makeTokenParser

makeTokenParser は GenLanguage データ型の値を引数にとり、GenTokenParser 型のデータを返す。GenLanguage 型も GenTokenParser 型も複数のフィールドをもつ単純なレコードだ。したがって、フィールド名のアクセサで fieldname data とすればフィールドの値を取り出すことができる。

GenLanguageDef

GenLanguageDef 型と GenTokenParser 型のフィールド名はそれぞれ次のようになる。GenLnaguageDef レコードのフィールドには、GenTokenParser を設定するためのパラメータを記述する。それを元に makeTokenParser 関数で GenTokenParser 型のデータを得るのが目的だ。

data GenLanguageDef s u m
= LanguageDef {
commentStart :: String,
commentEnd :: String,
commentLine :: String,
nestedComments :: Bool,
identStart :: ParsecT s u m Char,
identLetter :: ParsecT s u m Char,
opStart :: ParsecT s u m Char,
opLetter :: ParsecT s u m Char,
reservedNames :: [String],
reservedOpNames:: [String],
caseSensitive :: Bool
}

data GenTokenParser s u m = TokenParser { identifier :: ParsecT s u m String, reserved :: String -> ParsecT s u m (), operator :: ParsecT s u m String, reservedOp :: String -> ParsecT s u m (), charLiteral :: ParsecT s u m Char, stringLiteral :: ParsecT s u m String, natural :: ParsecT s u m Integer, integer :: ParsecT s u m Integer, float :: ParsecT s u m Double, naturalOrFloat :: ParsecT s u m (Either Integer Double), decimal :: ParsecT s u m Integer, hexadecimal :: ParsecT s u m Integer, octal :: ParsecT s u m Integer, symbol :: String -> ParsecT s u m String, lexeme :: forall a. ParsecT s u m a -> ParsecT s u m a, whiteSpace :: ParsecT s u m (), parens :: forall a. ParsecT s u m a -> ParsecT s u m a, braces :: forall a. ParsecT s u m a -> ParsecT s u m a, angles :: forall a. ParsecT s u m a -> ParsecT s u m a, brackets :: forall a. ParsecT s u m a -> ParsecT s u m a, squares :: forall a. ParsecT s u m a -> ParsecT s u m a, semi :: ParsecT s u m String, comma :: ParsecT s u m String, colon :: ParsecT s u m String, dot :: ParsecT s u m String, semiSep :: forall a . ParsecT s u m a -> ParsecT s u m [a], semiSep1 :: forall a . ParsecT s u m a -> ParsecT s u m [a], commaSep :: forall a . ParsecT s u m a -> ParsecT s u m [a], commaSep1 :: forall a . ParsecT s u m a -> ParsecT s u m [a] }

GenTokenParser 型のデータのフィールドには、それぞれ、フィールド名に対応するパーサが格納されている。例えば integer フィールドには整数をパースするパーサ、parens フィールドには括弧の間の部分を取り出すパーサといった具合だ。

したがって、自分のプログラム言語用のトークンパーサを作りたかったら、まず GenLanguageDef レコードを作成し、makeTokenParser でトークンの詰まった GenTokenParser 型のデータを作成する。個々のパーサは、それぞれのパーサのフィルド名で取り出して使うという手順になる。

GenTokenDef

ところで、GenTokenDef レコードは0から作成する必要はない。Text.Parsec.Language モジュールにはデフォールトの GenLanguageDef 型のレコードがいくつか準備されておりそれをカスタマイズするだけでトークンパーサを作ることができるような環境になっている。

emptyDef

デフォールトの GenLanguageDef 設定レコードの中で、最もシンプルなものは emptyDef だ。これを使ってどんなパーサが作れるかを実験してみる。

Text.Parsec.Token は qualified インポートで別名をつける。GenTokenParser のフィールド名と同じパーサを作りたいからだ。その意味は次の実行例を見れば分かる。

Prelude> import Text.Parsec
Prelude Text.Parsec> import qualified Text.Parsec.Token as P
Prelude Text.Parsec P> import Text.Parsec.Language (emptyDef)
Prelude Text.Parsec P Text.Parsec.Language> lexer = P.makeTokenParser
 emptyDef

lexer

この lexer の中に色々なパーサが含まれている。どんなパーサが含まれているかは、上のフィールド名のリストをみれば分かる。

それでは整数をパースする integer パーサを作ってみよう。

Prelude Text.Parsec P Text.Parsec.Language> integer = P.integer lexer
Prelude Text.Parsec P Text.Parsec.Language> parseTest integer "123"
123

文字列 "123" が integer でパースされて整数の 123 が返されるのが分かる。それでは '+' というシンボルをパースするパーサを作ってみよう。

Prelude Text.Parsec P Text.Parsec.Language> plus = (P.symbol lexer) "+"
Prelude Text.Parsec P Text.Parsec.Language> parseTest plus "+"
"+"

それでは整数と整数の足し算をするパーサ add を定義して実行してみる。

Prelude Text.Parsec P Text.Parsec.Language> add = (+) <$> integer <*>
 (plus >> integer)
Prelude Text.Parsec P Text.Parsec.Language> parseTest add "123 + 456"
579

注目すべきは integer と plus のパーサがちゃんとトークン間の空白の処理をしてくれていることである。

このように、自分のプログラム用の言語処理のパーサを作るときは makeTokenParser 関数を積極的に利用して省力化したほうがいい。makeTokenParser を使いこなせるようになって初めて Parsec 使いと言えるのではないだろうか。