パーサーコンビネータの実装 パート2

シリーズのパート2へようこそ。ここではHaskellを使ってシンプルなパーサーコンビネータライブラリを一から実装します。第一回目では、Parser 型を定義し、それに必要な全ての型クラスを実装しました。ソースはこちらで確認できます。それに加えて、いくつかのコンビネータを定義し、次のようなパーサーを定義しました。

pKv :: Parser KV
pKv = KV <$> parseKv
  where
    parseKv = (,) <$> many alpha <* char ':' <* spaces <*> many alpha

このシンプルなパーサーは、キーと値のペアをHaskellのデータ型に変換します。また、パーサーを_run_することもできます。

runParser pKv "key: value" -- Right (KV ("key","value"))

これはすでにかなり便利ですが、もっとリッチな感じにするために、さらにコンビネータを実装しましょう。前に定義した_pKv_ パーサーを拡張して、キーと値のペアのリストを簡単に解析できるようになるといいですね。_sepBy_というコンビネータを定義すれば、これは簡単です。

sepBy1 :: Parser a -> Parser b -> Parser [a]
sepBy1 p sep = (:) <$> p <*> many (sep *> p)

sepBy :: Parser a -> Parser b -> Parser [a]
sepBy parser separator = sepBy1 parser separator <|> pure []

_strict_バージョンを使用して_sepBy_を定義したことに注意してください。もし解析する_kv_が1つだけの場合、_sepBy1_は失敗しますが、_sepBy_は喜んで解析します。

ここで、_kv_のリストを解析するパーサーを定義するのは簡単です。

pKvs :: Parser [KV]
pKvs = pKv `sepBy` char ','

runParser pKvs "key: value,foo: bar" -- Right [KV ("key","value"),KV ("foo","bar")]

素晴らしいですね。そういえば、数値も解析できるようになるといいですね。そう思います!

import Data.Maybe (fromMaybe)
import Data.Applicative (Alternative(..), optional) 
import Data.Functor (($>))

digit :: Parser Char
digit = satisfy "digit" Char.isDigit

decimal :: (Integral a, Read a) => Parser a
decimal = read <$> many1 digit

signedDecimal :: Parser Int
signedDecimal = fromMaybe id <$> optional (char '-' $> negate) <*> decimal

これで数字を解析できるようになりました、万歳!こちらがいくつかの例です。

runParser (decimal @Int) "123" -- Right (123)
runParser (decimal @Int) "-123" -- Left "Expecting digit at position 0"
runParser signedDecimal "-123" -- Right (-123)

特に_レキサー_を書くときに、空白文字を解析するのは一般的なタスクです。_レキシング_をもっと便利にするためにいくつかの便利なコンビネータを定義しましょう。

spaces :: Parser ()
spaces = void $ many (satisfy "whitespace" Char.isSpace)

newline :: Parser ()
newline = char '\n' <|> (char '\r' *> char '\n')

horizontalSpaces :: Parser ()
horizontalSpaces = void . many $
  satisfy "horizontal whitespace" $ \c ->
    Char.isSpace c && c /= '\n' && c /= '\r'

先程解析したキーと値のペアはコンマで区切られていましたが、改行で区切られたキーと値のペアを解析したくなるかもしれません。

key: value
foo: bar

これに対するパーサーを書きます。

pKvsNewLine :: Parser [KV]
pKvsNewLine = pKv `sepBy` newline

runParser pKvsNewLine "key: value\nfoo: bar" -- Right [KV ("key","value"),KV ("foo","bar")]

あるいは、コンマで区切られたが間にランダムなスペースがある_kv_を解析したいかもしれません。例えば "key: value , foo: bar" のような。_horizontalSpaces_を使えばできます!

pKvsHorizontal :: Parser [KV]
pKvsHorizontal = pKv `sepBy` s
  where s = horizontalSpaces *> char ',' <* horizontalSpaces

runParser pKvsHorizontal "key: value       ,        foo: bar" -- Right [KV ("key","value"),KV ("foo","bar")]

この時点で、かなりパワフルなツールを手に入れました。まだ、この機械で面白いユースケースを見ていませんが、次回はシンプルなプログラミング言語のためのパーサーを定義します。

読んでいただき、ありがとうございました。楽しんでいただけたら幸いです。良い一日を!

  • Joona

パート1
私の他のブログ投稿

こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/japiirainen/implementing-parser-combinators-pt-2-3bae