强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

OCaml 教程 / PPX 预处理器与扩展

PPX 预处理器与扩展

PPX 是 OCaml 的元编程系统,它允许你在编译期间对代码进行转换。通过 PPX,你可以自动生成 boilerplate 代码、添加内联测试、实现自定义语法扩展等。


1. PPX 概述

1.1 什么是 PPX

PPX(PreProcessor eXtension)是 OCaml 的 AST 级别的预处理器:

源代码 → OCaml Parser → PPX → OCaml Compiler → 字节码/原生码
组件说明
ppxlibPPX 开发的标准库
ppx_deriving自动派生插件框架
ppx_inline_test内联测试框架
ppx_expect期望测试
ppx_sexp_convS-expression 序列化

1.2 PPX 的两种形式

(* 属性式 PPX *)
type t = { name : string; age : int; } [@@deriving show]

(* 扩展式 PPX *)
let%test "addition" = 1 + 1 = 2

let%expect_test "print" =
  print_endline "hello";
  [%expect {| hello |}]
语法作用域示例
[@attr]表达式/类型expr [@attr]
[@@attr]绑定/声明type t = ... [@@attr]
[@@@attr]结构体/签名[@@@warning "-32"]
[%ext expr]表达式扩展[%ext expr]
let%ext ...绑定扩展let%ext x = ...

2. Deriving 插件(ppx_deriving)

2.1 show / eq / ord

type point = { x : int; y : int; } [@@deriving show, eq, ord]

let p = { x = 3; y = 4 }
let () = print_endline (show_point p)
(* { x = 3; y = 4 } *)

2.2 make

type user = { name : string; age : int; email : string; } [@@deriving make]

let u = make_user ~name:"Alice" ~age:30 ~email:"[email protected]" ()

2.3 常用插件

插件功能生成函数
show转字符串show_<type>
eq相等比较equal_<type>
ord排序比较compare_<type>
make构造函数make_<type>
sexpS-expressionsexp_of_<type>
yojsonJSONto_yojson / of_yojson
iter迭代iter_<type>
fold折叠fold_<type>

2.4 复杂类型派生

type tree =
  | Leaf of int
  | Node of tree * tree
[@@deriving show, eq]

let t = Node (Leaf 1, Node (Leaf 2, Leaf 3))
let () = print_endline (show_tree t)

3. 内联测试(ppx_inline_test)

3.1 基本用法

let add x y = x + y

let%test "add positive" = add 2 3 = 5
let%test "add zero" = add 0 0 = 0
let%test "add negative" = add (-1) 1 = 0

let%test_unit "with assertion" =
  let result = add 10 20 in
  [%test_eq: int] result 30

(* 运行: dune runtest *)

3.2 测试模块

let%test_module "math tests" = (module struct
  let%test "1 + 1 = 2" = 1 + 1 = 2
  let%test "2 * 3 = 6" = 2 * 3 = 6
end)

3.3 带注解的测试

let%test "slow test" [@tags "no-js"] = true
let%test "linux only" [@tags "linux"] = true
let%test "expected failure" [@expect failure] = failwith "boom"

4. 期望测试(ppx_expect)

4.1 基本用法

let%expect_test "simple print" =
  print_endline "hello world";
  [%expect {| hello world |}]

let%expect_test "multiple lines" =
  print_endline "line 1";
  print_endline "line 2";
  [%expect {|
    line 1
    line 2 |}]

4.2 更新期望

# 自动更新期望值
dune runtest --auto-promote

# 手动更新
dune promote

💡 提示:期望测试非常适合测试编译器输出、日志输出、错误消息等。不需要手动编写断言,直接比对输出。


5. Sexp 序列化(ppx_sexp_conv)

5.1 基本用法

open Sexplib0

type address = {
  street : string;
  city : string;
  zip : string;
} [@@deriving sexp]

type person = {
  name : string;
  age : int;
  address : address;
} [@@deriving sexp]

let p = {
  name = "Alice"; age = 30;
  address = { street = "123 Main St"; city = "Anytown"; zip = "12345" };
}

let s = sexp_of_person p
let () = print_endline (Sexp.to_string_hum s)

5.2 可选字段和默认值

type config = {
  host : string; [@default "localhost"]
  port : int; [@default 8080]
  debug : bool; [@default false]
} [@@deriving sexp]

⚠️ 注意点ppx_sexp_conv 依赖 sexplib0 库,需在 dune 中添加 (libraries sexplib0) 依赖。


6. 自定义 PPX

6.1 使用 ppxlib 创建扩展

(* trace_ppx.ml *)
open Ppxlib

let () =
  Driver.register_transformation "trace"
    ~rules:[
      Context_free.Rule.extension
        (Extension.declare "trace"
          Expression
          Ast_pattern.(single_expr_payload __))
        (fun ~loc ~path expr ->
          [%expr
            let __result = [%e expr] in
            Printf.printf "[TRACE] %s\n"
              (Marshal.to_string __result []);
            __result
          ])
    ]

6.2 Dune 配置

; PPX 库
(library
 (name trace_ppx)
 (kind ppx_rewriter)
 (libraries ppxlib))

; 使用 PPX 的项目
(library
 (name my_project)
 (preprocess (pps trace_ppx)))

7. AST 结构

7.1 主要类型

类型说明示例
expression表达式1 + 2f x
pattern模式x(a, b)
type_declaration类型声明type t = ...
structure_item结构项let x = 1

7.2 使用 Ast_builder

open Ppxlib
open Ast_builder.Default

(* 类型安全的 AST 构造 *)
let make_constant loc n = eint loc n
let make_string loc s = estring loc s
let make_tuple loc exprs = pexp_tuple loc exprs

(* 构造函数定义 *)
let build_function loc =
  let x = pvar loc "x" in
  let body = evar loc "x" in
  value_binding loc ~pat:(pvar loc "id") ~expr:(eabstract loc [x] body)

7.3 AST 遍历

let transform_mapper = object
  inherit Ast_traverse.map as super

  method! expression expr =
    match expr.pexp_desc with
    | Pexp_constant (Pconst_integer (s, None)) ->
      let loc = expr.pexp_loc in
      { expr with pexp_desc = Pexp_constant (Pconst_float (s ^ ".0", None)) }
    | _ -> super#expression expr
end

8. PPX 最佳实践

8.1 开发建议

建议说明
使用 ppxlib不要直接用 ocaml-migrate-parsetree
最小化变换只修改需要修改的部分
保留位置信息loc 必须正确传播
提供有用的错误使用 Location.errorf
测试边缘情况空列表、单元素等

8.2 常见陷阱

(* ❌ 忘记 loc *)
(* let bad_expr = eint 42 *)
let good_expr = eint loc 42

(* ❌ 不保留位置信息 *)
let good_transform expr =
  { expr with pexp_desc = ... }  (* 保持 pexp_loc 不变 *)

8.3 调试技巧

# 查看 PPX 之后的代码
ocamlfind ocamlc -dsource -ppx "ppx_deriving src/foo.ml" foo.ml

# ppxlib 调试模式
PPXLIB_DEBUG=1 dune build

9. 常用 PPX 生态

PPX用途安装
ppx_deriving派生 show/eq/ordopam install ppx_deriving
ppx_inline_test内联测试opam install ppx_inline_test
ppx_expect期望测试opam install ppx_expect
ppx_sexp_convSexp 序列化opam install ppx_sexp_conv
ppx_janeJane Street 合集opam install ppx_jane
ppx_hashHash 函数opam install ppx_hash
ppx_compare比较函数opam install ppx_compare
ppx_bench基准测试opam install ppx_bench

10. 业务场景:自动生成验证器

(* 假设我们有一个 validate PPX *)
(* type user = {
  name : string;  [@validate String.length > 0]
  email : string; [@validate contains "@"]
  age : int;      [@validate >= 0 && <= 150]
} [@@deriving validate] *)

(* 手动生成的等价代码 *)
type user = { name : string; email : string; age : int; }

let validate_user u =
  let errors = [] in
  let errors =
    if String.length u.name > 0 then errors
    else "name must not be empty" :: errors in
  let errors =
    if String.contains u.email '@' then errors
    else "email must contain @" :: errors in
  let errors =
    if u.age >= 0 && u.age <= 150 then errors
    else "age must be 0-150" :: errors in
  match errors with [] -> Ok () | errs -> Error errs

let () =
  let good = { name = "Alice"; email = "[email protected]"; age = 30 } in
  let bad = { name = ""; email = "no-at"; age = 200 } in
  Printf.printf "good: %b\n" (validate_user good = Ok ());
  Printf.printf "bad: %b\n" (Result.is_error (validate_user bad))

11. 扩展阅读

资源说明
ppxlib 文档ppxlib.ocaml.org
ppx_deriving READMEGitHub 仓库
OCaml AST 文档compiler-libs 中的 Parsetree
Jane Street PPXppx_jane 文档
“Metaprogramming in OCaml”工业实践分享

💡 提示:PPX 很强大,但也容易被滥用。好的经验法则是:如果一个 PPX 只是为了减少几行 boilerplate,可能不值得引入额外复杂度。最好的 PPX 场景是:序列化/反序列化、测试、日志记录等重复性极高的工作。