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 のライブラリは関数がブラックボックスになりにくいので面白い。