OCaml PPX
概要・導入
基本
OCaml の文法と機能を拡張する方法。
PPXはOCamlコンパイラへのプラグインとして利用する。
ppx_regexpを使って、match
のような構文で正規表現のマッチが書ける。
match%pcre somestring with
| "^foo" -> some_expression1
| "(bar){2,3}" -> some_expression2
| _ -> some_default
%pcre
とか [@@ ...]
のようなマークがPPXが動作するための目印。
type point3d = float * float * float [@@deriving show]
で point3d
型にプリティープリント用の show
関数が自動定義される。
どう動いてるか
- AST組み立て時に
%foo
とか@@bar
とかのマークを記録する。 - 各PPXはAST中のマークを検索する。
- マークを発見したらASTを書き換える
その他
-
#...
な文字列はPPX用の演算子として予約済み -
int
,float
のリテラルに続いて[g..z|G..Z]
の範囲の1文字が続く表記はPPX用として予約済み-
1g
とか999Z
とか
-
内部動作の基礎
以下↑の記事の要約
let x = {id|\(^o^)/|id}
let () = prerr_endline x
この x.ml
が内部でどのようにASTになっているのかを確認する
$ ocamlc -dparsetree x.ml
[
structure_item (x.ml[1,0+0]..[1,0+23])
Pstr_value Nonrec
[
<def>
pattern (x.ml[1,0+4]..[1,0+5])
Ppat_var "x" (x.ml[1,0+4]..[1,0+5])
expression (x.ml[1,0+8]..[1,0+23])
Pexp_constant Const_string ("\\(^o^)/",Some "id")
]
]
...
PPXの動作を確認する
-ppx
オプションでPPXを実装したコマンドを指定する
$ ocamlc -ppx コマンド名 x.ml
コマンドは command <infile> <outfile>
の形式で infile → outfile
を生成する。なお出力ファイルはバイナリ形式。
以下の疑似コマンドを定義する
#!/bin/sh
# x.sh
cp $1 $2
cp $1 /tmp/copy.bin
以下のように呼び出せる → ocamlc -ppx 'sh x.sh' x.ml
上記の動作を理解した上で OCaml で PPXコマンドを書く
(**
* 何もしないPPXコマンド
*)
let infile = Sys.argv.(1)
let outfile = Sys.argv.(2)
let ic = open_in_bin infile
let oc = open_out_bin outfile
(* ASTの定義モジュール *)
open Parsetree
(* ASTをを確認する関数 *)
let filter f =
(* ASTのヘッダを確認 *)
let header =
let buf = "Caml1999M016" in
let len = String.length buf in
assert (input ic buf 0 len = len);
buf
in
Location.input_name := input_value ic;
let v = input_value ic in
close_in ic;
let v = f header v in
output_string oc header;
output_value oc Location.input_name;
output_value oc v;
close_out oc
(* エントリーポイント *)
let () =
filter (fun _header v -> v)
上記のコマンドの呼び出しは以下
$ ocamlfind ocamlc -package compiler-libs.common -linkpkg -o filter filter.ml
$ ocamlc -ppx ./filter x.ml
実際になにかするPPXコマンド
- 実際にASTをいじるのは
Ast_mapper
モジュールを使う
let infile = Sys.argv.(1)
let outfile = Sys.argv.(2)
open Parsetree
open Asttypes
open Ast_mapper
let my_mapper = { default_mapper with
expr = (fun mapper -> function
| ( { pexp_desc = Pexp_constant (Const_string (s, Some "id")) } as e) ->
(* {id|xxx|id} の `xxx` を2倍にする操作 *)
{ e with pexp_desc = Pexp_constant (Const_string (s ^ s, None)) }
| e -> default_mapper.expr mapper e)
}
let () = apply ~source:infile ~target:outfile my_mapper
PPXイントロダクション at 2019
PPX は OCaml の AST を操作するバイナリ。
- 捜査対象である OCaml のAST定義は
Parsetree
モジュールにある。
より詳細なドキュメントは compiler-libs/parsetree.mli
を読むべき。
PPX で重要になるAST型
式
let foo = ...
の ...
の部分
(* 評価されて値になる式を表す *)
type expression = {
pexp_desc : expression_desc;
pexp_loc : Location.t;
pexp_loc_stack : location_stack;
pexp_attributes : attributes;
}
パターン
let ... = f ()
の ...
の部分やパターンマッチなど。
(* ある構築物から OCaml の値へ分解する型 *)
type pattern = {
ppat_desc : pattern_desc;
ppat_loc : Location.t;
ppat_loc_stack : location_stack;
ppat_attributes : attributes;
}
型表現
.mli
ファイルの val f : ...
の ...
の部分
type core_type = {
ptyp_desc : core_type_desc;
ptyp_loc : Location.t;
ptyp_loc_stack : location_stack;
ptyp_attributes : attributes;
}
構造体やシグネチャの表現
(* 構造体表現 *)
type structure = structure_item list
type structure_item = {
pstr_desc : structure_item_desc;
pstr_loc : Location.t;
}
(* シグネチャ表現 *)
type signature = signature_item list
type signature_item = {
psig_desc : signature_item_desc;
psig_loc : Location.t;
}
実際のASTの確認
ppx_tools
を使うとすぐに確認できて便利。
PPXで実際に変換したいASTがどこかを調べて開発すると早い。
$ ocamlfind ppx_tools/dumpast some_file.ml
# もしくは
$ ocamlfind ppx_tools/dumpast -e "1 + 1"
もしくは ocamlc
や utop
に -dparsetree
オプションでも見れる。
PPX の種類
大まかに分けて2種類ある
拡張系PPX
該当箇所のASTを「書き換える」系の動作を行う。
おおよそ [%<拡張名> payload]
の形式で記述。
例:
-
ppx_getenv
-
[%getenv SOME_ENVVAR]
でコンパイル時に該当箇所を環境変数に置き換える拡張
-
let () =
match [%getenv "PPX_GETENV2"] with
| None -> ()
| Some _ -> ()
let () = assert ([%getenv "DOES_NOT_EXIST"] = None)
-
ppx_yojson
- OCamlの構文で
Yojson
を書けるようにする拡張-
[%yojson {a = None; b = 1}]
→{"a": null, "b": 1}
と変換
-
- OCamlの構文で
派生系(derivers)PPX
該当箇所にASTを「追加する」系の動作を行う。
おおよそ [@@deriving <deriver_name>]
の形式で記述
例:
-
ppx_deriving
-
[@@deriving show,eq]
とかすると比較関数や表示関数が自動定義される -
ppx_deriving
自体にもプラグインがあり、さらにいろいろなderiving
の派生物がある。
-
-
ppx_deriving_yojson
-
ppx_deriving
のプラグイン- JSONシリアライザが追加定義される
-
type t =
{ a: int
; b: string [@default ""] (* デフォルト値を指定してオプショナル化 *)
}
[@@deriving of_yojson]
-
ppx_deriving_sexp
-
JaneStreet
のSexpLib
に基づいたS式シリアライザを追加定義するppx_deriving
プラグイン
-