OCaml4.02から、CamlP4が独立したリポジトリで管理されることになり、これから拡張類はPPXという仕組みで作っていくことが推奨されそうな雰囲気です。
一応CamlP4も触ってみていた個人としては、結構気になっていたので、実際に触ってみました。
PPXとは
PreProcessor eXtensionの略(多分)です。
今までは拡張するといったらCamlP4でがんばってLexerの拡張とかParserの拡張とかを書く必要がありましたが、これからはCamlP4で書くかわりにPPXで書くことが推奨されます。(と思われる)
PPXの拡張は、後でみる通り普通にOCamlとして書くことができるので、CamlP4の難解なRevised Parseのlexerのmliとにらめっこする必要がない、というのが利点となると思います。
代わりに、後述するように、文法そのものを拡張することはできません。なので、若干拡張の自由度、というものは減ります。
ただし、その分公式にはQuatationとかそういったものが用意されていないので、わりかし面倒な感じもします。
OCamlでの拡張の埋め込み方
OCaml4.02から、文法として以下のものが取り込まれました。
[@...]
[@@...]
[@@@...]
これをAttributeと呼び、式とかstructureとかに付与することができます。[@...]
が式、[@@...]
がstructure、[@@@...]
がソース全体に対するAttributeです。
[%...]
[%%...]
こっちをExtensionと呼び、これもまた式とかstructureとかに付与することができます。
AttributeとExtensionの違いは、Attributeは、何もしなければ中の構造ごとOCamlのパーサーから無視されますが、Extensionについては、対応する拡張がコンパイル時に存在しなければ、Parse Errorになります。
これらについて、OCamlの構文木をいじくって新しい構文木にする、というのがPPXでの拡張の作り方になります。
実際に書いてみた
rspecみたいなBDD風味に書けるようなCamlP4拡張を自分で作っていたので、これをPPXで書き直してみました。
元々はこんな感じで書けるようにしていました。
describe "spec" begin
it "test" begin
1 should = 1
end
end
これについて、Parser拡張して文法を追加して〜という感じにやっていました。
で、今回試しにやってみたところ、とりあえずこんな感じになりました。
let %spec "spec" =
1 [@should (=) 1];
let f = fun a b = a > b in
1 [@should f 2]
上記は、実際には以下の書き方のシンタックスシュガーです。
[%spec
let "spec" =
1 [@should (=) 1];
let f = fun a b = a > b in
1 [@should f 2]
]
(シンタックスが対応してませんでした・・・)
コンパイル時
コンパイルには、compiler-libsが基本的に必要になります。リンクするcmaとかが必要になるので、ocamlfindを利用することを前提です。
$ ocamlfind ocamlc -package compiler-libs.common -o ppx_hoge hoge.ml
こんな感じになります。実際には.commonを渡す必要がありますのでその辺が若干注意です。
ppxとして利用するファイルは、実際にはこのコンパイルで作成された実行ファイルが対象になります。
拡張を利用したコードのコンパイル時
上記で作成したものを利用するとして、以下のようになります。
$ ocamlfind ocamlc -ppx ppx_hoge foobar.ml
-ppxに渡すのは、上記で作成した実行ファイルのパスになります。
実際のソース(の一部)
実際のソース全体はそれなりのサイズなので、ここでは一番大事なTreeの再構築部分になります。
まずは [%spec...]
のパース部分です。
(* この辺はやっておかないとめちゃめんどいです *)
open Ast_helper
open Ast_mapper
open Asttypes
open Parsetree
open Longident
open Location
let spec_mapper argv =
{ default_mapper with
structure_item = (fun mapper strc ->
match strc with
| {pstr_desc =
Pstr_extension (({ txt = "spec"; loc}, pstr),_);_} ->
begin match pstr with
| PStr [{pstr_desc = Pstr_value (_, [{pvb_pat = pat;pvb_expr = e;_}])}] ->
begin match pat with
| {ppat_desc = Ppat_constant (Const_string (str,_));_} ->
Str.eval ~loc
(Exp.apply ~loc (Exp.ident {txt = names_to_module_path ["Simplespec";"Spec";"add_spec"];loc})
[("", Exp.constant (Const_string (str, None)));
("", uncurry_fun ~loc [Pat.var {txt = "spec"; loc}] (should_mapper.expr mapper e))])
| _ -> failwith "spec must contain constant let "
end
| _ -> failwith "spec have to be extension for structure"
end
| _ -> default_mapper.structure_item mapper strc)
}
・・・長いですね。いいたいことはよくわかります。特に、Parse Treeをそれぞれでパターンマッチする必要があるため、その辺がわりと難解ですが、実際のParse Treeを確認する場合、コンパイラに-dparsetreeオプションをつけてコンパイルすると、上記のPpat_constant〜とかそういったものが表示されるので、それを参考にすることができます。
で、実際に上記でどんな感じに変換されるのかというと、上の例だと、
Simplespec.Spec.add_spec "spec" (fun spec ->
1;
let f = fun a b = a > b in
1
)
という感じになります。ここではとりあえず、@should についてはとりあえずここでおいておきますが、こういう風に、実際のソースを思い浮かべながらParsetreeモジュールで定義されている型をつなぎ合わせていくことになります。
上記で、Pstr_とかPpat_とかが、Parse Treeの要素で、Parsetreeモジュールで定義されています。ほかのopenしている module は、OCamlのソース中に含まれているので、それらを見ると、それなりにシンプルな型が並んでいます、が、実際にパターンマッチ中で展開すると、こんな感じになってしまう、ということです。
これはさすがに面倒なので、この部分を作成するための更なる拡張が、ppx_toolsというリポジトリで作られているのを見ました。
CamlP4でやっていたquatationとかの代わりに、OCaml標準で提供しているparse treeをいじるためのモジュールを利用して、こんな感じに書く、ということです。細かい部分は・・・実際にいじってみるのが一番です(ぶん投げ)
とりあえずいじってみて
今回はとりあえずやってみた、という感覚が非常に強かったですが、これまでのCamlP4でできた拡張は基本的にこれで解決できる(ということになっている)ようです。
今はまだ試行錯誤の段階にあるとおもいますので、またこのためのツールも発展していくのだと思いますが、この一番の基礎となる部分をいったん触っておく、というのもいいんじゃないでしょうか。
個人的には完全にパズル解いてる気分でした。