2013年10月3日木曜日

HaskellのParsecライブラリを使ってみる

HaskellのParsecの初歩的な使い方をまとめてみた。

Parsecとは?

Haskellのパーサコンビネータライブラリ。
これを使って、例えばJSONをパースするパーサを作ったり、ソースコードの構文解析をしたりできる。
正規表現の代わりに使うこともできる。

1文字だけパース(というか認識)するパーサを作る

おそらく最も簡単な1文字だけをパースするパーサを作ってみる。
つまり、入力文字がパーサが指定した文字とマッチするかどうかを判定するだけのパーサ。
これを作ることの実用的な意味は全くない。

手っ取り早く、インタプリタで動かしてみる。
  • インタプリタ起動
  • $> ghci

  • Parsecをインポートする
  • GHCI> import Text.ParserCombinators.Parsec

  • 文字'a'をパースする
  • GHCI> parseTest (char 'a') "a"
    'a'

    parseTestは、引数にパーサと入力文字列を受取り、パーサを実行する。
    この場合は、パーサはchar 'a'で、入力文字列は、"a"
    char 'a'というパーサの動作は、文字'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関数で実行する。

-- 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関数を少し改良して、コマンドラインから文字列を入力できるようにした。

-- 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を使えば、

do
 a <- char 'a'
 b <- char 'b'
 c <- char 'c'
 return $ a:b:c:[]


とも書くこともできる。

コードを書き換えて、実行してみる。

-- 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パーサをいくつも連結する必要はない。

-- 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"
というように使う。

とりあえず、ソースコードは次のようなものになった。

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 件のコメント:

コメントを投稿