HaskellのParsecの初歩的な使い方をまとめてみた。
これを使って、例えばJSONをパースするパーサを作ったり、ソースコードの構文解析をしたりできる。
正規表現の代わりに使うこともできる。
つまり、入力文字がパーサが指定した文字とマッチするかどうかを判定するだけのパーサ。
これを作ることの実用的な意味は全くない。
手っ取り早く、インタプリタで動かしてみる。
次のコードの3行目のようにパーサを定義することで実現できる。
まず、char 'a'で'a'とマッチさせ、return 'A'というパーサで返す文字を決めている。
この2つのパーサはモナドなので>>で、2つのパーサを連結することができる。
そして同じようにparseTest関数で実行する。
コンパイルして実行する。
実行結果をみると、ちゃんと'a'が'A'に変換されていることがわかる。
このようにして、Parsecでは小さなパーサを繋げていって大きなパーサを作っていく。
先と同じように1文字だけをパースするパーサを組み合わせれは、簡単に2文字をパースするパーサを作ることができる。
3文字のパーサだったら次のようになる。
次のコードをコンパイルして実行してみる。
main関数を少し改良して、コマンドラインから文字列を入力できるようにした。
ここで注意しなければならないのは、出力結果が'c'になっていること。
最後のパーサchar 'c'の戻り値が結果として出力される。
最初の2つのパーサの結果は捨てられている。
これらのパーサの結果を使いたければ、>>ではなく>>=を使えばよい。
または、doを使えば、
とも書くこともできる。
コードを書き換えて、実行してみる。
今度は、全てのパーサの結果を使えた。
ところで、Parsecライブラリには、上記のパーサを簡単に書ける関数が用意されていて、次のように書ける。
このstring関数は、引数に取る文字列をパースするパーサを作成してくれる。
これを使えば、charパーサをいくつも連結する必要はない。
同じ結果を得られる。
substr関数は、引数を3つ取る。
第一引数は、置換したい文字列。
第二引数は、置換後の文字列。
第三引数は、対象の文字列。
戻り値は、置換後の第三引数の文字列。
例えば、"hello world"という文字列の"hello"を"hi"に置換したい場合は、
というように使う。
とりあえず、ソースコードは次のようなものになった。
この関数の主な処理は、9行目に定義されているparserパーサが行っている。
1文字づつ処理していき、fromとマッチすればtoに変換し、マッチしなければ何もせずに1文字だけ進むことを繰り返しているパーサを表現している。
parserの中で、新たに定義されたパーサconvert, string1を使っている。
convertは、文字列fromとマッチしたら文字列toに変換するパーサ。
tryを使うことで、パースに失敗した場合は、入力文字列を消費しないようにしている。
string1は、どんな文字でも1文字マッチし、マッチしたChar型の文字をString型に変換するパーサ。
anyCharは、すべての文字をパースするパーサ。どんな文字もパースしたいときに使用する。
convertが失敗すれば、string1を適用したい場合は、「または」を意味する<|>を使う。
つまり、convert <|> string1のようにする。
これで、fromとマッチすればtoに変換し、マッチしなければ何もせずに1文字だけ進むパーサを作成できた。
ただし、これだけだったら入力文字列の第一文字めからfromにマッチするか、そうでなければ単に1文字だけパースするパーサになってしまい、入力文字列の途中にfromがあっても処理してくれない。
そこで、manyを使う。manyは、引数にとったパーサの0回以上の繰り返しをパースする。
これによって、fromにマッチしなければ、1文字進み、途中でもマッチすればtoに変換するパーサになる。
5行目のparse関数は、parseTestのようにパーサを実行するところは同じだが、若干の違いがある。
第一引数は、実行したいパーサ。
第二引数は、失敗した場合のメッセージのようなものを指定する。
第三引数は、パース対象の文字列。
戻り値は、パース結果を格納したEither。成功した結果は、Rightに、失敗した場合は、Leftにそれぞれ格納される。
これでsubstr関数の完成。
Parsecとは?
Haskellのパーサコンビネータライブラリ。これを使って、例えばJSONをパースするパーサを作ったり、ソースコードの構文解析をしたりできる。
正規表現の代わりに使うこともできる。
1文字だけパース(というか認識)するパーサを作る
おそらく最も簡単な1文字だけをパースするパーサを作ってみる。つまり、入力文字がパーサが指定した文字とマッチするかどうかを判定するだけのパーサ。
これを作ることの実用的な意味は全くない。
手っ取り早く、インタプリタで動かしてみる。
- インタプリタ起動
- Parsecをインポートする
- 文字'a'をパースする
- 'a'以外の文字を入力してみる
$> ghci |
GHCI> import Text.ParserCombinators.Parsec |
GHCI> parseTest (char 'a') "a" 'a' |
parseTestは、引数にパーサと入力文字列を受取り、パーサを実行する。
この場合は、パーサはchar 'a'で、入力文字列は、"a"。
char 'a'というパーサの動作は、文字'a'を受取り、'a'を返し、それ以外の文字を受け取った場合はエラーとなる。
ここでは、入力文字列が"a"なので、パースは成功し、'a'を返している。
GHCI> parseTest (char 'a') "b" parse error at (line 1, column 1): unexpected "b" expecting "a" |
入力文字列を"b"にしてみると、失敗していることがわかる。
「aを期待しているのにbが来てますよ」って言っている。
小文字のaを大文字のAに変換するパーサを作る
先に作ったchar 'a'というパーサは、ただ'a'という文字と入力文字がマッチするか判定するだけだったが、今度はマッチして、さらにそれを大文字に変換させるようにパーサを進化させてみる。次のコードの3行目のようにパーサを定義することで実現できる。
まず、char 'a'で'a'とマッチさせ、return 'A'というパーサで返す文字を決めている。
この2つのパーサはモナドなので>>で、2つのパーサを連結することができる。
そして同じようにparseTest関数で実行する。
1
2
3
4
| -- sample1.hs
import Text.ParserCombinators.Parsec
p = char 'a' >> return 'A'
main = parseTest p "a"
|
コンパイルして実行する。
$> ghc sample1.hs $> ./sample1 'A' |
実行結果をみると、ちゃんと'a'が'A'に変換されていることがわかる。
このようにして、Parsecでは小さなパーサを繋げていって大きなパーサを作っていく。
複数の文字をパースしてみる
今度は1文字だけ処理するパーサを、複数の文字を処理できるパーサへ拡張する。先と同じように1文字だけをパースするパーサを組み合わせれは、簡単に2文字をパースするパーサを作ることができる。
char 'a' >> char 'b' |
3文字のパーサだったら次のようになる。
char 'a' >> char 'b' >> char 'c' |
次のコードをコンパイルして実行してみる。
main関数を少し改良して、コマンドラインから文字列を入力できるようにした。
1
2
3
4
5
6
| -- sample2.hs
import Text.ParserCombinators.Parsec
p = char 'a' >> char 'b' >> char 'c'
main = do
input <- getContents
parseTest p input
|
$> ghc sample2.hs $> echo "abc" | ./sample2 'c' |
ここで注意しなければならないのは、出力結果が'c'になっていること。
最後のパーサchar 'c'の戻り値が結果として出力される。
最初の2つのパーサの結果は捨てられている。
これらのパーサの結果を使いたければ、>>ではなく>>=を使えばよい。
char 'a' >>= \a -> char 'b' >>= \b -> char 'c' >>= return $ a:b:c:[] |
または、doを使えば、
1
2
3
4
5
| do
a <- char 'a'
b <- char 'b'
c <- char 'c'
return $ a:b:c:[]
|
とも書くこともできる。
コードを書き換えて、実行してみる。
1
2
3
4
5
6
| -- sample3.hs
import Text.ParserCombinators.Parsec
p = char 'a' >>= \a -> char 'b' >>= \b -> char 'c' >>= \c -> return $ a:b:c:[]
main = do
input <- getContents
parseTest p input
|
$> ghc sample3.hs $> echo "abc" | ./sample3 "abc" |
今度は、全てのパーサの結果を使えた。
ところで、Parsecライブラリには、上記のパーサを簡単に書ける関数が用意されていて、次のように書ける。
string "abc" |
このstring関数は、引数に取る文字列をパースするパーサを作成してくれる。
これを使えば、charパーサをいくつも連結する必要はない。
1
2
3
4
5
6
| -- sample4.hs
import Text.ParserCombinators.Parsec
p = string "abc"
main = do
input <- getContents
parseTest p input
|
$> ghc sample4.hs $> echo "abc" | ./sampl43 "abc" |
同じ結果を得られる。
文字列置換するパーサ
ここまでは、ほとんど実用的なものではなかったので、少しだけ実用的なものとして、文字列を置換するsubstr関数を作成してみる。substr関数は、引数を3つ取る。
第一引数は、置換したい文字列。
第二引数は、置換後の文字列。
第三引数は、対象の文字列。
戻り値は、置換後の第三引数の文字列。
例えば、"hello world"という文字列の"hello"を"hi"に置換したい場合は、
substr "hello" "hi" "hello world" -- "hi world" |
とりあえず、ソースコードは次のようなものになった。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| -- sample5.hs
import Text.ParserCombinators.Parsec
substr :: String -> String -> String -> String
substr from to str = case parse parser "error" str of
Right x -> x
Left x -> str
where
parser = do
res <- many $ convert <|> string1
return $ foldl (\a -> \b -> a ++ b) "" res
convert = (try $ string from) >> return to
string1 = anyChar >>= \x -> return $ x:[]
|
この関数の主な処理は、9行目に定義されているparserパーサが行っている。
1文字づつ処理していき、fromとマッチすればtoに変換し、マッチしなければ何もせずに1文字だけ進むことを繰り返しているパーサを表現している。
parserの中で、新たに定義されたパーサconvert, string1を使っている。
convertは、文字列fromとマッチしたら文字列toに変換するパーサ。
tryを使うことで、パースに失敗した場合は、入力文字列を消費しないようにしている。
string1は、どんな文字でも1文字マッチし、マッチしたChar型の文字をString型に変換するパーサ。
anyCharは、すべての文字をパースするパーサ。どんな文字もパースしたいときに使用する。
convertが失敗すれば、string1を適用したい場合は、「または」を意味する<|>を使う。
つまり、convert <|> string1のようにする。
これで、fromとマッチすればtoに変換し、マッチしなければ何もせずに1文字だけ進むパーサを作成できた。
ただし、これだけだったら入力文字列の第一文字めからfromにマッチするか、そうでなければ単に1文字だけパースするパーサになってしまい、入力文字列の途中にfromがあっても処理してくれない。
そこで、manyを使う。manyは、引数にとったパーサの0回以上の繰り返しをパースする。
これによって、fromにマッチしなければ、1文字進み、途中でもマッチすればtoに変換するパーサになる。
5行目のparse関数は、parseTestのようにパーサを実行するところは同じだが、若干の違いがある。
第一引数は、実行したいパーサ。
第二引数は、失敗した場合のメッセージのようなものを指定する。
第三引数は、パース対象の文字列。
戻り値は、パース結果を格納したEither。成功した結果は、Rightに、失敗した場合は、Leftにそれぞれ格納される。
これでsubstr関数の完成。
0 件のコメント:
コメントを投稿