[Clojure]instaparseでパーサを作成

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

instaparseとは

今、Clojure使いの間では大きな話題となっている、パーサ(構文解析プログラム)作成用ライブラリです。
正規表現を使うのと同じくらい簡単にパーサが作成できるとのことです。
Clojure作者である、Rich Hickeyも大興奮(ソース:twitter)らしいです。

では簡単にinstaparseの特徴を解説します。
githubに機能一覧があるのですが、そこから代表的ないくつかの特徴を抜粋。

  • 入力として文字列を受け取り、EBNF記法を用いて構文解析ツリーを生成する
  • 出力フォーマットは、Clojureで人気の形式(hiccupとenlive)の両方をサポート
  • 解析エラーの詳細なレポートを出してくれる

文字列をEBNF記法でうけとってパーサが簡単に書けるというみたいです。
Githubのチュートリアルを参考に、instaparseを動かしてみましょう。

環境構築方法

今回使用した動作環境は以下のとおりです。

  • OS : MacOS X 10.7.5
  • Clojure : 1.5.1
  • Leiningen : 2.1.2

instaparseを使ってみる

まずはleiningenでプロジェクトを作成します。

% lein new insta

 

次に依存するライブラリをinsta/project.cljに記述しましょう。下記のように、instaparseを依存ライブラリに追加してください。

(defproject insta "HEAD"
  :dependencies [[org.clojure/clojure "1.5.1"]
                 [instaparse "1.0.1"]]
  :main insta.main)

project.cljを記述したらlein depsで依存ライブラリを取得しておきます。

% lein deps

では、src/insta/main.cljを下記のように記述しましょう。

(ns insta.main
(:require [instaparse.core :as insta]))

(def as-and-bs
  (insta/parser
    "S = AB*
     AB = A B
     A = 'a'+
     B = 'b'+"))

(defn -main [& args]
  (as-and-bs "aaabbaabbb"))

parser関数で渡している文字列が、構文解析ルールです。(BNFの詳細については割愛)
上記ルールにしたがって「aaabbaabbb」を解析させると、次のようになります。

% lein run 
[:S [:AB [:A "a" "a" "a"] [:B "b" "b"]] [:AB [:A "a" "a"] [:B "b" "b" "b"]]]

先ほどは出力形式がhiccup形式でしたが、enlive形式にすることもできます。
次のようにparserの引数として渡すか、set-default-output-format!関数を使用して:enliveをセットします。

;関数でセットするか
(insta/set-default-output-format! :enlive)

;もしくはparserの引数で渡す
(def as-and-bs
  (insta/parser
    "S = AB*
     AB = A B
     A = 'a'+
     B = 'b'+" :output-format :enlive))

これで実行すると、enlive形式でパース結果が出力されます。

% lein run
{:tag :S, :content ({:tag :AB, :content ({:tag :A, :content ("a" "a" "a")} {:tag :B, :content ("b" "b")})} {:tag :AB, :content ({:tag :A, :content ("a" "a")} {:tag :B, :content ("b" "b" "b")})})}

さて、次は構築されたツリーを変換する例をみてみましょう。
次の関数はスペース区切りの文章をアルファベット(:word)と数値(:number)にパースします。

(def words-and-numbers-one-character-at-a-time
  (insta/parser
    "sentence = token (<whitespace> token)*
     <token> = word | number
     whitespace = #'\\s+'
     word = letter+
     number = digit+
     <letter> = #'[a-zA-Z]'
     <digit> = #'[0-9]'"))

;呼び出し.main関数の中に記述してください
(words-and-numbers-one-character-at-a-time "abc 123 def")

実行すると、次のようにパースされますが、1文字ずつの文字として分割されているので、使いやすいとはいえません。

% lein run 
[:sentence [:word "a" "b" "c"] [:number "1" "2" "3"] [:word "d" "e" "f"]]

そういったケースにtransform関数を使用すると、パースされた結果の値に対して関数を適用した結果を取得できます。
次の記述では、:wordの結果は文字列として連結を、:numberの結果は、文字列連結した後、数値に変換しています。

;transformで変換.main関数の中に記述してください
(insta/transform
     {:word str,
      :number (comp read-string str)}
     (words-and-numbers-one-character-at-a-time "abc 123 def"))

実行してみましょう。結果が使いやすくなりました。

% lein run     
[:sentence "abc" 123 "def"]

;なお、enlive形式だとこうなる
;{:tag :sentence, :content ("abc" 123 "def")}

まとめ

さて、今回はClojureのパーサ作成ライブラリを紹介しました。
私はBNFやパーサについては詳しくないのですが、その私でも簡単なサンプルなら動かすことができたので、
かなりすごいライブラリなのではないでしょうか。

参考サイトなど