Open6

OCaml PPX

zenwerkzenwerk

概要・導入

https://ocamlverse.github.io/content/ppx.html

基本

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 関数が自動定義される。

どう動いてるか

  1. AST組み立て時に %foo とか @@bar とかのマークを記録する。
  2. 各PPXはAST中のマークを検索する。
  3. マークを発見したらASTを書き換える

その他

  • #... な文字列はPPX用の演算子として予約済み
  • int, float のリテラルに続いて [g..z|G..Z] の範囲の1文字が続く表記はPPX用として予約済み
    • 1g とか 999Z とか
zenwerkzenwerk

内部動作の基礎

https://github.com/camlspotter/ocaml-zippy-tutorial-in-japanese/blob/master/ppx.md

以下↑の記事の要約

x.ml
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を生成する。なお出力ファイルはバイナリ形式。

以下の疑似コマンドを定義する

copy.sh
#!/bin/sh
# x.sh
cp $1 $2
cp $1 /tmp/copy.bin

以下のように呼び出せる → ocamlc -ppx 'sh x.sh' x.ml

上記の動作を理解した上で OCaml で PPXコマンドを書く

filter.ml
(**
 * 何もしない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 モジュールを使う
filter.ml
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
zenwerkzenwerk

PPXイントロダクション at 2019

https://tarides.com/blog/2019-05-09-an-introduction-to-ocaml-ppx-ecosystem

PPX は OCaml の AST を操作するバイナリ。

より詳細なドキュメントは 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"

もしくは ocamlcutop-dparsetree オプションでも見れる。

zenwerkzenwerk

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}と変換

派生系(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
    • JaneStreetSexpLib に基づいたS式シリアライザを追加定義する ppx_derivingプラグイン