OCaml 教程 / PPX 预处理器与扩展
PPX 预处理器与扩展
PPX 是 OCaml 的元编程系统,它允许你在编译期间对代码进行转换。通过 PPX,你可以自动生成 boilerplate 代码、添加内联测试、实现自定义语法扩展等。
1. PPX 概述
1.1 什么是 PPX
PPX(PreProcessor eXtension)是 OCaml 的 AST 级别的预处理器:
源代码 → OCaml Parser → PPX → OCaml Compiler → 字节码/原生码
| 组件 | 说明 |
|---|
ppxlib | PPX 开发的标准库 |
ppx_deriving | 自动派生插件框架 |
ppx_inline_test | 内联测试框架 |
ppx_expect | 期望测试 |
ppx_sexp_conv | S-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> |
sexp | S-expression | sexp_of_<type> |
yojson | JSON | to_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 + 2、f 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/ord | opam install ppx_deriving |
ppx_inline_test | 内联测试 | opam install ppx_inline_test |
ppx_expect | 期望测试 | opam install ppx_expect |
ppx_sexp_conv | Sexp 序列化 | opam install ppx_sexp_conv |
ppx_jane | Jane Street 合集 | opam install ppx_jane |
ppx_hash | Hash 函数 | 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 README | GitHub 仓库 |
| OCaml AST 文档 | compiler-libs 中的 Parsetree |
| Jane Street PPX | ppx_jane 文档 |
| “Metaprogramming in OCaml” | 工业实践分享 |
💡 提示:PPX 很强大,但也容易被滥用。好的经验法则是:如果一个 PPX 只是为了减少几行 boilerplate,可能不值得引入额外复杂度。最好的 PPX 场景是:序列化/反序列化、测试、日志记录等重复性极高的工作。