今年の夏が暑すぎて、しばらくアウトプットをしていないことに気が付きました。28℃でも全く止まらないエアコンが怖い。
だいぶ前に、OCamlに入ったPPXについての記事を書きました。
最近、自分で利用するために作っているライブラリで、PPXを使って文法拡張しようとしました。その際、「そう言えば最近だとPPXってどう書くんだ?」と疑問を抱いたので、調べてみました。
ちなみに、以下のブログで非常に詳細に語られているので、まずこっちを読むことをオススメします。
PPXって何?
OCamlerな人でも、利用している人がいたりいなかったりするみたいです。CamlP4が黒魔術状態だったので気持ちはわかります。要は色々な拡張を書くために言語に組み込まれたextension pointです。
PPXについてのOCaml Labの記事。
http://ocamllabs.io/doc/ppx.html
camlspotterさんの記事。必読。
https://github.com/camlspotter/ocaml-zippy-tutorial-in-japanese/blob/master/ppx.md
2018年で利用するツール・ライブラリ
-
opam
- 必須。まず入れましょう
-
dune
- 完全にデファクトになった感があります
- lwtとかの有名所も使ってるので、とりあえず四の五の言わずに使っておきましょう
-
ppxlib
- ppx_toolsだったりocaml-migration-parsetreeだったりが統合されました
- duneとの連携がデフォルトだったり、色々と便利になっているので、問答無用で入れておきましょう
去年の段階で、ビルドツールは基本dune一択状態になりつつあります。opam + duneが標準構成となり、昔の百花繚乱時代(そして苦しむ時代)は過去のものになったかと思います。
dune + ppxlibでのppxの作り方
こっからは拙作のライブラリから例を引っ張り出します。今回のサンプルはこれです。
何かと言うと、ここ数ヶ月ずっとjs_of_ocaml + React.jsをやっている間に、色々と勉強を兼ねて作ったjs_of_ocaml用のReact.jsバインディングです。
bucklescriptだったりreasonmlであれば、reason-reactとかありますが、OCamlで慣れた側からすると普通にOCamlで書けるjs_of_ocamlの方が楽(か?)です。
opamファイルを作る
さて、まずはopamファイルを作ります。duneはopamを前提にしているっぽく、opamファイルが無いとまず怒られます。opamファイルの名前と、ライブラリ名は合わせた方が得策です。
opam-version: "2.0"
version: "0.2.0"
maintainer: "derutakayu@gmail.com"
authors: "derui"
license: "MIT"
homepage: "https://github.com/derui/jsoo_react"
bug-reports: "https://github.com/derui/jsoo_react"
dev-repo: "git+https://github.com/derui/jsoo_react"
tags: []
build: [
["dune" "build" "-p" name "-j" jobs]
["dune" "runtest" "-p" name ] {test}
]
depends: [
"dune" {build & >= "1.0.0"}
"js_of_ocaml" { >= "3.0.2"}
"js_of_ocaml-ppx" { >= "3.0.2"}
"mocha_of_ocaml" {test}
"snap-shot-it_of_ocaml" {test}
"ppxlib" { >= "0.2.1" }
]
available: ["ocaml" >= "4.05.0"]
色々ありますが、 ppxlib と dune が重要です。
duneファイル
次に、ppxライブラリのduneファイルです。
https://github.com/derui/jsoo_reactjs/blob/master/src/ppx/dune
(library
(name ppx_jsoo_reactjs)
(public_name jsoo_reactjs.ppx)
(kind ppx_rewriter)
(libraries ppxlib)
(preprocess (pps ppxlib.metaquot)))
ここで重要なのは、 (kind ppx_rewriter)
と、 (libraries ppxlib)
です。 kind
の方は、METAファイルをいい感じにしてくれるやつで、これを入れておくと、 (pps)
がいい感じにppxの実行ファイルを一つにまとめてくれます。
この辺がduneの良し悪しで、ppxlib(もしくはppx_driver)を利用せずに作られたppxの実行ファイルは、ちょっと特殊な感じに書く必要があります。
実際、この制約があってうまい具合に動かないppxがあったようです。
opam + jbuilderが広まってからOCamlを始めた方は、多分METAファイルって何?という感じになるでしょう。METAファイルは、dune/jbuilderが実際にコンパイルを行う際に呼び出している、 ocamlfind
が利用する設定ファイルです。
だいぶ昔は私も手書きしていましたが、今はツールに任せましょう。
実際にppx extensionを書いてみる
では、実際にextensionを書いてみましょう。このextesionは、次のような変換が行われます。
let span = [%e span ~key:"foo" ~class_name:"foobar"]
(* 上が↓こうなります *)
let span = Jsoo_reactjs.create_dom_element ~key:"foo" ~props:(Jsoo_reactjs.element_props ~class_name:"foobar" ())
上の変換を行うextensionは、リポジトリ上では以下で定義しています。
↓のソースは、一部足りないのでそのままだと動きません。雰囲気を感じてくれれば。
let expand ~loc ~path:_ (ident : longident) args =
let dom = match ident with
| Lident id -> Some id
| _ -> None
in
match dom with
| None -> [%expr ident args]
| Some dom -> begin
(* 実際にソースを生成します。ここで生成されたソースが、ppxを通した後にextension部分と置換されます。 *)
let f = [%expr Jsoo_reactjs.create_dom_element] in
let args = build_args loc args in
let dom = Ast_builder.Default.estring ~loc dom in
let args = (Nolabel, dom) :: List.rev args |> List.rev in
Ast_builder.Default.pexp_apply ~loc f args
end
let ext =
(* extensionを定義してます。自動的に、"e" というextensionかつ、payloadがAst_pattern.(...) 部分にマッチするような
matchを作ってくれます。
以前はここも自前で書いていました。
*)
Extension.declare
"e"
Extension.Context.expression
Ast_pattern.(single_expr_payload (pexp_apply (pexp_ident __) __))
expand
(* register_transformationで、自作のextensionを構成します *)
let register () = Driver.register_transformation name ~extensions:[
ext;
]
ソースの中で、 [%expr ident args]
みたいにしているのが、metaquotというextensionです。ppxで式だったり何だったりを構成するのはかなり面倒くさく、時に非直感的です。
metaquotを使うと、面倒な部分が結構削減できます。使っておきましょう。
ここではシンプルに見えますが、色々とやろうとすると、その式にマッチするようなmatchをゴリゴリ書いていくことになります。ppxlibの中に、そういう時に楽ができそうなものもありそうですので、利用してみるのもいいかと思います。(試してない
ppxのテスト
いくつかあるとは思いますが、ppxは基本的にソースを書き換えるだけです。なので、変換前後のソースでdiffを取る、というのも一つのテストです。
ただ、ppxを通すとコンパイルできなくなる場合があります。なので、利用シーンを想定したテストを書いておくのがいいかと思います。
自分で作ったextensionをテストする場合、以下のようなduneファイルになります。
(alias
(name runtest)
(deps ppx_test.bc.js)
(action (run npm run test)))
(executable
(name ppx_test)
(libraries js_of_ocaml jsoo_reactjs mocha_of_ocaml.async snap-shot-it_of_ocaml)
(preprocess (pps js_of_ocaml-ppx jsoo_reactjs.ppx)))
duneファイルについては、なんのことは無い、ppsに作ったppx extensionのライブラリ名を指定するだけです。
テストについては、色々ありますが、今回はJavaScriptとして実行してなんぼなので、自作したmochaバインディングを使っています。
(* ... *)
"can create element via ppx extension" >:: (fun () ->
let module C = R.Component.Make_stateless(struct
class type t = object end
end)
in
let t = C.make (fun _ -> [%e span ["text"]]) in
let renderer = new%js R.Test_renderer.shallow_ctor in
renderer##render (R.create_element t);
let output = renderer##getRenderOutput in
snapshot(output);
assert_ok true
);
(* ... *)
ppxを作るときのデバッグtips
ppx extensionでバグがある場合、割と探しづらいです。ppxlibを利用すると、作られた実行ファイルに色々とデバッグ向けの便利機能が付きます。
ただ、今回使った限り、-dtypedtree
がなかったので、ちょっと使いづらかったです。
-dparsetree
はありましたが、 -dparsetree
の場合、どうもppxlibのドライバ部分も入ってしまっている?ようで、かなり長い上に読みづらいです。
個人的には、ppx extensionを開発する場合、 -dtypedtree
が必須状態です。このオプション、undocumentedなオプションなんですが、このオプションを指定した時の出力が、ppxに渡されるASTとほぼ1:1になっているのです。
具体的には、こういうソースが
let a = (1,2,3)
こうなります。
[
structure_item (tmp.ml[1,0+0]..tmp.ml[1,0+15])
Tstr_value Nonrec
[
<def>
pattern (tmp.ml[1,0+4]..tmp.ml[1,0+5])
Tpat_var "a/1002"
expression (tmp.ml[1,0+8]..tmp.ml[1,0+15])
Texp_tuple
[
expression (tmp.ml[1,0+9]..tmp.ml[1,0+10])
Texp_constant Const_int 1
expression (tmp.ml[1,0+11]..tmp.ml[1,0+12])
Texp_constant Const_int 2
expression (tmp.ml[1,0+13]..tmp.ml[1,0+14])
Texp_constant Const_int 3
]
]
]
ppx extensionではこういうのをmatchしたり組み立てたりするので、どういう式がどういうASTになっているか?をお手軽に確認できるので、ホントよく使います。割と使っている人を見かけないので、他の人はどうやってるんだろう・・・。
結び
今回、自分のppxに対する知識のアップデートも兼ねて、ppx extensionの作り方をさらっとなぞってみました。環境が整ってきて、こういうものも作りやすくなったように思います。
こうみても、OCamlも始めやすくなりました。4年前のppx extensionはビルドも手探りでしたから。
自分が触り始めた3.12くらい(多分7年とか前)では、ビルドツールも定まっておらず、ライブラリのバージョン固定も開発者次第でした。拡張もCamlP4だったり、それをビルドするのも辛かったり、という時代です。
その分、色々と詳しくなれることはなれるんですが、いかんせん敷居が高かったです。
今も変わらないのか、QiitaでOCamlのタグがOculusより少ない時点でなんというか・・・。
今はopam + duneがほぼ鉄板となったことや、JaneStreet Coreの充実っぷりもあり、かつてないほどOCamlが始めやすく、実用に供せられると思います。
Windows?何のことやら・・・。
今回はppx extensionでしたが、今度は最近のjs_of_ocamlについてとか書きたいなーと思ってます。
今年はガチで危険を感じる暑さなので、命大事にいきましょう。