# 宏
Deft 宏在编译期运行,接收未求值的参数,并生成拼接进调用点的代码。它们是这门语言主要的代码生成工具——用于控制结构、DSL,以及消除样板代码。
## 定义宏
`defmacro` 遵循与 `def` 相同的 `|params| body` 语法。宏体在编译期运行并返回 Deft 代码。
defmacro double {|x| ($x + $x)}
当你调用 `double 5` 时,编译器会把宏体展开为 `(5 + 5)` 并就地编译。
### 参数
宏参数是未求值的。参数值就是来自调用点的字面 AST 节点:
defmacro when {|cond body| [if $cond $body]} when ($n > 5) { echo "big" } # expands to: # if ($n > 5) { echo "big" }
### 可变参数宏 —— `@rest`
`@` 前缀把剩余参数收集到一个列表中。`@` 对应 `@{...}` 向量字面量——它表示“收集列表的参数”。必须放在最后。`@` 之后的名称按原样绑定——挑一个在调用点读起来最自然的即可:
defmacro variadic-sum {|@nums| [reduce $nums 0 {|a b| ($a + $b)}]} variadic-sum 1 2 3 4 # → 10
在普通的 `def` 中同样适用,并且可与默认值组合:
def f {|a b=10 @rest| ($a + $b + [len $rest])}
## 引用与反引用
`[macro/quote ...]` 在构建代码时保留 `$param` 引用为字面符号。`[macro/unquote $param]` 是转义——它把参数的值替换进被引用的形式中。两者都必须使用 `[...]` 方括号(deft 的 `( ... )` 圆括号仅用于中缀,不是调用)。
defmacro unless {|cond body| [macro/quote [if [macro/unquote $cond] nil [macro/unquote $body]]] } unless false { echo "ran" } # expands to: [if false nil { echo "ran" }] # → prints "ran"
大多数宏不需要显式的 `macro/quote`——通过 `$a` 进行的参数拼接已经能生成正确的代码。当你需要从零生成新代码,或希望某个 `$param` 作为字面符号保留到展开结果中时,才使用 `macro/quote`。
### `[macro/syntax-quote ...]` —— 卫生的代码生成
类似 `macro/quote`,但:
1. `$param` 引用会被替换(无需在参数周围使用 `[macro/unquote ...]`)。
2. 裸符号(例如 `tmp`)会自动重命名为唯一的 gensym 名称,因此它们不会捕获调用方的变量。
defmacro swap {|a b| [macro/syntax-quote { set tmp $a # ← 'tmp' auto-gensym'd to tmp#N set $a $b set $b $tmp }] } set tmp "outer" [swap x y] echo $tmp # still "outer" — safe
### `[macro/splice $list]` —— 内联列表元素
在宏展开期间把列表的元素展开到父形式中:
defmacro call-with {|func @args| [macro/quote [macro/unquote $func [macro/splice $args]]] } [call-with str "a" "b" "c"] # expands to [str "a" "b" "c"]
(不要与 `..$xs` 混淆,那是用于函数调用的运行时展开运算符——是另一项独立特性。)
## 常见模式
### 自定义控制结构
defmacro unless {|cond body| [if $cond nil $body]} unless ($n == 0) { echo "non-zero" }
### 宏中间的块体
`{...}` 块参数会被内联到 `$body` 出现的位置,即使它不是最后一条语句也是如此——宏会就地运行它:
defmacro sandwich {|body| set before "before" $body set after "after" } set inside "" [sandwich {set inside "inside"}] # inside == "inside"
### 循环
defmacro my-repeat {|n body| for _ [range $n] $body } my-repeat 3 { echo "hi" }
### 函数生成
defmacro make-adder {|n| def adder {|x| ($x + $n)} } make-adder 10 adder 5 # => 15
### `[begin e1 e2 ...]` —— 顺序执行
按顺序求值每个表达式,并返回最后一个。多语句的 `{...}` 块会自动包装进 `begin`,因此很少显式使用——它在 `macro/quote`/`macro/syntax-quote` 内部用于多步展开时最有用:
defmacro inc-and-return {|x| [macro/quote [begin [set $x ($x + 1)] $x]] }
## 编译期条件 —— `macro/compile-when`
`[macro/compile-when cond body]` 在编译期(通过 VM)求值 `cond`,仅当为真时才生成 `body`。若为假,`body` 被丢弃——调用点编译为一个 `push_null`,产生零运行时代码。
这与 `if` 不同,后者总是编译两个分支并在运行时选择。`macro/compile-when` 适用于编译期即可确定决策、且被拒绝分支不应有任何开销的场景:按级别过滤的日志、仅调试用的断言、特性开关控制的代码路径。
def log-level ([os/env "DEFT_LOG_LEVEL"] or "debug") defmacro debug {|@args| [macro/compile-when [in? @{"debug"} $log-level] [log/debug [macro/splice $args]]] }
当 `DEFT_LOG_LEVEL=error` 时,每个 `[debug ...]` 调用点都编译为 `push_null`——`log/debug` 调用永远不会进入字节码。
条件必须能在编译期求值:字面量、来自 `require` 的模块的全局量,以及由它们构成的纯函数。它不能引用运行时局部变量或闭包捕获。完整细节见 macro/compile-when。
## 卫生性
Deft 宏默认不是卫生的——宏引入的符号会与调用点的同名符号冲突。有两种工具可以解决:
- `[macro/gensym "prefix"]` —— 返回唯一的关键字(`:prefix#N`)。可用作不透明的运行时标记或映射键。不能成为变量名(deft 的 `set` 接受字面标识符)。
- `[macro/syntax-quote ...]` —— 在编译期对裸符号自动 gensym,使生成的标识符保持新鲜。这是实现编译期卫生的正确工具。
## 内置宏
若干内置形式以宏或编译期特殊形式实现:
- `defsuite` / `deftest` —— 测试脚手架(见 Testing)
- `def` / `defn` / `defcoro` / `deftask` / `deftype` / `defprotocol` /
`defimpl` / `defmethod` / `defmacro` —— 所有定义形式
- `if` / `for` / `while` / `match` / `try` / `catch` / `defer` —— 控制流
- `macro/quote` / `macro/unquote` / `macro/syntax-quote` / `macro/splice` / `begin` —— 代码生成
- `macro/compile-when` —— 编译期条件(省略被拒绝的分支)
## 何时使用宏
宏增强了能力,但降低了可读性。在以下情况才使用宏:
- 你需要一个不符合函数调用语义的新控制结构(`unless`、`when`、`cond`)。
- 你需要以闭包无法清晰表达的方式延迟求值参数。
- 你在构建看起来像语法本身的 DSL(HTTP 路由、测试脚手架、HTML 块)。
当函数可以胜任时,不要编写宏:
# ✗ Unnecessary macro defmacro add {|a b| ($a + $b)} # ✓ Just a function def add {|a b| ($a + $b) }
## 调试宏展开
要查看宏展开成什么,使用 `info/sig` 或 `info/source`:
[info/sig $when] [info/source $when]
要查看调用点的完整展开,把调用包在 `macro/quote` 中并美化打印:
[pretty [macro/quote { when ($x > 5) { echo "big" } }]]
## 局限性
- 宏的递归不能超过编译器的展开限制(约 64 层)。
- 宏无法访问运行时状态——它们在编译期运行。
- 宏体可以调用其他宏;展开是递归的。
- 宏必须返回有效的 Deft 表达式。字符串、数字以及其他非 AST 值会导致编译错误。
## 快速参考
| 形式 | 用途 |
|---|---|
| `defmacro name { \|params\| body }` | 定义宏 |
| `@rest` | 可变参数(收集剩余参数)——`@` 之后的名称按原样绑定 |
| `[macro/quote expr]` | 构建代码,保留 `$param` 为字面量 |
| `[macro/unquote $expr]` | 在 `macro/quote` 内部进行替换 |
| `[macro/splice $list]` | 把列表的元素内联到父形式中 |
| `[macro/syntax-quote expr]` | 卫生的 quote——替换 `$param`,对裸符号进行 gensym |
| `[macro/gensym "prefix"]` | 生成唯一的 `:prefix#N` 关键字 |
| `[begin e1 ... en]` | 顺序执行表达式,返回最后一个 |
| `[macro/compile-when cond body]` | 编译期条件——若 `cond` 为假则省略 `body` |
| `[info/sig $macro]` | 查看宏信息 |
| `[info/source $macro]` | 查看源码 |
完整的反射/元数据 API 见 Metaprogramming。