Parsec 入門


Parsec コンビネータ sepBy, endBy

sepBy1

sepBy1 は "foo,bar,buz" のようなコンマ区切りの文字列をリストにするパーサコンビネータだ。
Prelude Text.Parsec> parseTest (sepBy1 (many1 letter) (char ',')) "foo,bar,buz"
["foo","bar","buz"]

sepBy1 がどのように動作するかは Hackage でソースを見ると分かる。そこで my_sepBy1 を定義して動作を見てみる。

Prelude Text.Parsec> my_sepBy1 p sep = (:) <$> p <*> many (sep >> p)

動作を確認すると sepBy1 と同じように働くことが分かる。

Prelude Text.Parsec> parseTest (my_sepBy1 (many1 letter) (char ',')) "foo,bar,buz"
["foo","bar","buz"]

ソースを見ると sepBy1 がどのような動作をするのかが分かる。つまり、最初に p とマッチングを行い、次に many (sep >> p) をマッチさせる。many は0回以上のパターンの繰り返しだから、sepBy1 は p, p >> (sep >> p), p >> (sep >> p) >> (sep >> p), ... とパターンマッチが行われることが分かる。

ここから sepBy1 は p だけにもマッチするが、p のマッチが失敗するとエラーになることが分かる。

Prelude Text.Parsec> parseTest (sepBy1 (many1 letter) (char ',')) "foo"
["foo"]
Prelude Text.Parsec> parseTest (sepBy1 (many1 letter) (char ',')) "123"
parse error at (line 1, column 1):
unexpected "1"
expecting letter

sepBy のソースは sepBy1 を利用して次のように定義されている。

Prelude Text.Parsec> my_sepBy p sep = my_sepBy1 p sep <|> return []

これは sepBy1 でパターンマッチが失敗したら return [] で空リストを返す。

Prelude Text.Parsec> parseTest (my_sepBy (many1 letter) (char ',')) "123"
[]

endBy1

endBy1 のソースは次のようになる。

Prelude Text.Parsec> my_endBy1 p sep = many1 $ p <* sep

これは、p >> sep というパターンの1回以上の繰り返しにマッチすることが分かる。この my_endBy1 は endBy1 と同じ動作をする。

Prelude Text.Parsec> parseTest (my_endBy1 (many1 letter) (char ';')) "foo;bar;buz;"
["foo","bar","buz"]

endBy は endBy1 の定義の many1 を many に変えて定義されている。

Prelude Text.Parsec> my_endBy p sep = many $ p <* sep

この my_endBy は次のように endBy と同じ動作をするが、

Prelude Text.Parsec> parseTest (my_endBy (many1 letter) (char ';')) "foo;bar;buz;"
["foo","bar","buz"]

最初のマッチに失敗してもエラーにならず [] を返す。

Prelude Text.Parsec> parseTest (my_endBy (many1 letter) (char ';')) "123"
[]

sepBy, sepBy1, endBy, endBy1 の動作は Hackage の説明を見てもよくわからないが、ソースを見ると、その仕組みや、動作の特徴の差がよく分かる。また、sepBy1 や endBy1 のほうが発想的には sepBy や endBy より先であるのが分かる。Haskell のライブラリは関数がブラックボックスになりにくいので面白い。