Camlp4の文法拡張について
梅雨にかかわらず、記録的な少雨になりそうとか不吉なことが囁かれていて、今から夏が不安です。というか今から暑いのは勘弁してくださいほんと。
そんな中、ふとやってみたくなったことがありました。前に、Kaputtを紹介しましたが、BDD的なツールとして、RSpecをインスパイアしたOSpecというものも存在します。使ってはいないんですが、サンプルによるとこんなコードでテストができるようです。
https://github.com/andrenth/ospec/blob/master/examples/features.ml
describe "The number one" do it "should equal 2 when added to itself" do (1 + 1) should = 2 done; it "should be positive" do let positive x = x > 0 in 1 should be positive; 1 should be (fun x -> x > 0); 1 should be (fun x y -> x > y) 0 done; it "should be negative when multiplied by -1" do let x = 1 * (-1) in x should be < 0; x should not be >= 0 done; it "should raise when divided by 0" do let f = (fun () -> 1 / 0) in f should raise_an_exception; f should raise_exception Division_by_zero; f should not raise_exception Exit done; it "should match ^[0-9]+$ when converted to a string" do (string_of_int 1) should match_regexp "^[0-9]+$" done; it "should be cool" done
さて、これを見ると、通常のOCamlソースではないことは一目瞭然だと思います。
- do〜doneがfor/whileじゃないのに使えてる
- shouldとかbeとかが中置演算子みたいに使えてる
- 関数適用とかの順序がなんか違う
- =や>=が()でくくらなくても使えてる
こんなことは、Camlp4を使わないと不可能です。type_convのwithとかも多分Camlp4で文法拡張していると思います。
こういうのを見るとやってみたくなるのが、メインストリームじゃないもの(OCamlがメインストリームじゃないとは思いませんが)に興味を持つ人間の性ではありますが、Camlp4の情報がいかんせん少ないです。特に日本語情報が少なすぎて涙が出そうなので、とりあえず調べた結果を書き連ねていこうかと思います。
最終的には、簡単なSpecを書けるように(なれればいいなぁ)。
なお、以下ではCamlp4のquotationとかについては割愛します。確か前にこのブログで書いた気が・・・。
Camlp4の文法拡張について
Camlp4では、文法(Grammar)についても拡張することができます。
拡張は、以下のような形式で定義することができます。
EXTEND Gram ... END
重要なのは当然ながら...の部分なので、以下でそれを(私が調べた範囲で)詳しく書いていってみたいと思います。
文法の拡張方法
EXTEND Gram
...
END
の...にあたる部分(以降では文法定義とします)についてですが、これは基本的に以下のような構成となっています。
文法要素名 : {LEVEL "ラベル"} [{"ラベル"} [ 要素のBNF的な定義 -> 文法要素の結果の型となる値 | 要素のBNF的な定義 -> 文法要素の結果の型となる値 ... ] | {"ラベル"} [...] ]
つまり、その終端記号と認識するためのBNF的な定義にマッチした場合に、結果となる型を提供する、という
形になります。
二重になっている[]ですが、これがいまいち私も理解していないのですが、最外部のの内部は、定義されたの単位で、先に定義された方が低いレベルとして、パーサーの中で優先順位が下がる、という形になっているようです。
その前に付与されるラベルは、他の文法定義の中で利用するためのラベルとして利用されます。付与しなくてもOKです。
[]の前に付いているLEVEL "ラベル"ですが、これは基本的には無くても問題無いのですが、既存の文法を拡張するときとかは、これによって拡張先の文法を指定する、という感じ・・・だと思います(汗
BNF的な定義は、以下のような要素(termとでも呼ぶんでしょうか)を;で区切った形式で構成されます。ほかにもあるんでしょうが、あまり必要な気がしません。
- 文字列
- ソース上の文字列とそのまま対応する
- 束縛変数名 = 型
- その位置にある値が、指定した型である場合に、その型の値を変数に束縛する
`束縛変数名 = 型`の型の部分には、以下のようなtermを入れることができます。
- LIST0 term
- 指定した終端記号が0個以上。LIST0 〜 SEP ","のようにして、区切りを指定することもできる。また、termの部分は、[]で囲むことで、他の終端記号の組み合わせで構築することもできる
- STRING
- 文字列
- INT
- 整数
- FLOAT
- 浮動小数点数
- 文法要素名
- 指定した文法要素
Gramの直後の終端記号は、quotationとして指定することで、その内部が上記で指定したGrammarであるとしてパースしてくれます。
なお、BNF的な定義中で""で囲んだ部分については、実際のソース上では""が付かない形のものとして認識されます。
新規文法の作成
標準文法の作成だけではなく、新しいquotationsを一から作成することもできます、が、ここでは省略します。JSONを自動的にvalidなOCamlに変換する、とかもできるようですが、ここでは割愛します。またやってみたくなったら書くと思います。
文法拡張を含む場合のコンパイル
ocamlfindを利用する前提として、通常のcamlp4のコンパイルのフラグに加えて、以下のフラグを指定する必要があります。
- -package camlp4.extend
例として、myparser.mlというファイルに拡張文法を記載した場合のコンパイルは以下のようになります。
内部でquotationsを利用する場合は、packageに更にcamlp4.quotations.oとかを加える必要がありますが。
$ ocamlfind ocamlc -package camlp4.extend,camlp4.lib -syntax camlp4o myparser.ml
文法拡張が動作しているかのチェック方法
camlp4ofを使い、以下のような形で実施します。
$ camlp4of -parser <作成したcmo> <対象のml/mli>
結果は標準出力に出力されるので、それを確認しましょう。上手くいかなくても泣かない。
最小のサンプル
ここでは、OCamlの基本文法を拡張することを念頭に置いて、最も簡単な拡張のサンプルを記述してみます。
open Camlp4.PreCast open Syntax EXTEND Gram expr: LEVEL "simple" [ ["foo" -> <:expr<"foo+bar">>] ]; END
これは、fooというidentityがあった場合、それをfoo+barという「文字列」に展開するような文法を新規に定義しています。文字列の部分をexpr quotatonで囲んでいるのは、exprという文法要素が、ASTを返すことを要求しているため、これをやっておかないとそもそもコンパイルエラーになります。
これのコンパイルは、extendとquotatonを含んでいるため、以下のような形になります。(これをmyparser.mlというファイルに記述した場合)
$ ocamlfind ocamlc -package camlp4.quotations.o,camlp4.extend,camlp4.lib -syntax camlp4o myparser.ml
で、できたこれを
let a = foo ^ "bar"
というソースをsample.mlというファイルに保存した場合、以下のようにして結果を確認できます。
camlp4of -parser myparser.cmo sample.ml
出力結果は以下のようになります。
let a = "foo+bar" ^ "bar"
これくらいわかれば、後はquotationとantiquotationを駆使することでできるようです・・・。うん、そう簡単にいきませんよね。でもこれが大体わかったら、なんとなくですがソースは読めるようになりました。
とりあえず終わり
この記事の目的は、Camlp4の文法拡張方法についてまとめることでしたので、とりあえずこんな感じ、ということを書き連ねました。Camlp4については、実際に利用されているソースを見るのが一番の勉強になると思います、ほんと。
でも、最初のとっかかりがあれば、その調べる時間でいろいろ試すこととかもできるはずなので、なんかやってみたい人の最初の一歩になれればいいなー、と思います。というか自分がまず一歩を踏み出さんと。