# 宏 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。