[入門]関数プログラミング―質の高いコードをすばやく直感的に書ける!

第5章 パーサコンビネータ―小さなパーサを組み合わせて大きなパーサを作る

この記事を読むのに必要な時間:およそ 4 分

escapedを実装する

次に,BNFで一番長い部分であるリスト1(4)を実装します。次のようになります。

escaped :: Parser String
escaped = dquote *>
          many (textdata <|> comma <|> cr <|> lf
            <|> try (dquote *> dquote))
          <* dquote

コンビネータ*>は,左のパーサを実行したあとに右のパーサを実行し,右のパーサの結果を返します。一方,<*は同じ実行順序ですが,左のパーサの結果を返します。このことから,二重符号で囲まれた部分に対し,両端の二重符号を捨てて,中身を返していることがわかります。

コンビネータmanyは,引数を0回以上繰り返すためのコンビネータで,リストを返します。manyの引数では,textdataやcommaが<|>で区切られて列挙されているのがわかります。このコンビネータは,BNFでは前述の通り/に当たります。

tryの意味

最後に列挙されているのは,

try (dquote *> dquote)

です。これは2DQUOTEを実現しています。このコードは,二重符号が2回連続すると,1つだけ(どちらでもよいがこの場合は右)返すことを意味します。では,なぜtryで囲まれているのでしょうか?

Parsecでは,パースが成功し消費された入力データは忘れ去られます。しかし,tryで囲んだ場合は,入力データを覚えていて,パースが失敗した場合は入力を巻き戻し,ほかの可能性を探ります。

escapedの最初には,あとで定義する二重符号文字をパースするdquoteパーサがありますから,tryの部分が試されるのは,二重符号で囲まれた中です。このとき現れる二重符号には2通りの可能性があります。

  • ① 二重符号文字を表すための連続した二重符号
  • ② 囲みの終わりを示す二重符号

仮にtryがないコードを書いて,囲みの終わりを示す二重符号が来た場合のことを考えてみましょう。この場合,(dquote *> dquote)の左側は成功します。その二重符号は忘れさられます。次に右側は失敗するので,manyが終了し,escapedの最後のdquoteが試されます。しかし,もうその二重符号は忘れ去られているので,うまくパースできません。

このように(dquote *> dquote)の右側が失敗する場合は,左側が消費した二重符号をもとに戻さなくてはいけません。それを実現するのがコンビネータtryの役割です。

nonEscapedとtextdataを実装する

nonEscapedやtextdataは簡単に定義できます。

nonEscaped :: Parser String
nonEscaped = many textdata

textdata :: Parser Char
textdata = oneOf (" !" ++ ['#'..'+'] ++ ['-'..'~'])

oneOfは文字通り,引数に取ったリスト中の文字のどれかにマッチするというコンビネータです。++はリストを連結する演算子です。

残りを実装する

あとは,前述のdquoteも含め,文字をパースするパーサを作れば完成です。これにはcharを使います。charは引数に文字を取り,その文字をパースするパーサを返します(文字という型を表すCharとは違うものです⁠⁠。

comma :: Parser Char
comma = char ','

crlf :: Parser Char
crlf = cr *> lf

lf :: Parser Char
lf = char '\x0a'

cr :: Parser Char
cr = char '\x0d'

dquote :: Parser Char
dquote = char '"'

使ってみる

定義したCSVのパーサを使ってみましょう。

% ghci csv.hs

対話的に使うには,parseTestにパーサと入力文字列を与えます。

> parseTest csv "boo,\"foo,woo\",goo\r\nboo,\"foo\"\
"woo\",goo\r\n" 実際は1行
[["boo","foo,woo","goo"],["boo","foo\"woo","goo"]]

うまくパースできましたね。今回作った各部品も同じ方法で利用できます。

コンビネータライブラリの真価

パーサのコンビネータライブラリを使うと,BNF通りにパーサが書けることがわかったと思います。今回はトップダウン的に書きましたが,通常はボトムアップ的に作っていきます。つまり,小さなパーサを定義し,テストして確信が持てたら,それらをコンビネータで組み上げて大きなパーサを作ります。

ここで強調しておきたいのは,コンビネータライブラリを使って書いたコードは,何も特別なものではなく,単にその言語で書いたコードになります。そのため,静的型付きの関数型言語であれば,コンパイル時に型検査の恩恵が受けられるのです。

さらに勉強するには

駆け足で関数プログラミングを紹介しました。

初めて関数プログラミングに触れた人に興味を持っていただいたり,挫折した人が再び挑戦する気になっていただいたり,⁠実用的ではない」と勘違いしていた人が「けっこう使えるのかも」と思っていただけたりしたとしたら,幸いです。

これからも勉強していきたい人のために,アドバイスを書いておきます。このアドバイスは,何も関数型言語に限った話ではなく,どのプログラミング言語にも言えることです。

関数型言語を1つ選ぶ

まず,複数の関数型言語を調べてみて,どれか1つ言語を選んでください。直感的にクールだと思った言語がよいでしょう。また,ブログやTwitterのやりとりを観察して,優しく教えてくれる人が多い言語を選ぶとよいかもしれません。よく知らないで「関数型言語の人は恐い」と言っている人もいますが,関数型言語の熟練者には優しい人が多いと思います。

情報を集めコードを書いてみる

言語を決めたら,複数の本を読んでみましょう。1冊では駄目です。そして,実際にコードを書いてみましょう。また,公開されているブログ記事やチュートリアルもたくさん読みましょう。

既存の関数を再発明してみる

次に,基本ライブラリにどんな関数が定義してあるかを把握し,それらがどのように使われているのか調べます。

使い方がわかったあとは,自分でその関数を再発明してみましょう。この作業は,とても理解を深めてくれます。

勉強会に参加する

それから,勉強会に参加しましょう。最近は関数型言語の勉強会がたくさん開催されています。知り合いがいなくても,思い切って参加しましょう。そして,たくさん質問しましょう。

Happy functional programming!

著者プロフィール

山本和彦(やまもとかずひこ)

株式会社IIJイノベーションインスティテュート(IIJ-II)技術研究所 主幹研究員。

開発した代表的なオープンソフトにMew,Firemacs,Mightyがある。『プログラミングHaskell』や『Haskellによる並列・並行プログラミング』の翻訳者。職場ではHaskell,家庭では3人の子供と格闘する日々を送っている。

web:http://www.mew.org/~kazu/

twitter:@kazu_yamamoto