Clojure 中的函数和宏
由于在其他编程语言里很少接触宏(macro),只接触过 C 语言里的宏,即 #define。C 语言里的宏在编译器预处理时被处理,由于我没有 C 的开发经验,所以最常用宏的场景仅仅是用 #define 来定义常量。因此,在学习 Clojure 时,我对函数和 Lisp 语言里常见的宏产生了些许疑问。
函数和宏到底有什么区别?在 Clojure 里,它们都以相似的语法被定义。
;; 这是一个函数
(defn do-something [arg] ...)
;; 这是一个宏
(defmacro my-macro [arg] ...)
调用函数和使用宏的方式也一致。
(do-something arg)
(my-macro arg)
而且它们也都支持用 & 来标记可变数量的参数。
区别
最终在 StackOverflow1 上找到了解答。
简单来说,函数和宏被调用时:
- 函数的参数会先被计算(evaluate),然后再用计算得到的值执行函数体。
- 宏会先进行宏展开,将当前代码变成宏定义的形式,再计算整段代码。
以下是几个说明这种区别的例子。
一
用于逻辑运算的 or 是宏,并且不能是函数,因为 (or a b) 在计算时,如果 a 的结果已经为 true,就不会继续计算 b。如果 or 是函数,那么就要把 a 和 b 都计算之后才会计算 or 函数本身,这对逻辑运算来说是低效且不合理的。
二
由于函数的参数会先被计算,所以参数不允许出现无效的表达式,而宏则是允许的。
设有一个宏 (mymacro (1 2 +)) 会被展开为 (+ 1 2),而 (1 2 +) 本身是无效的,因为没有 1 这个函数或者宏。假如 mymacro 是函数而不是宏,就会报错,因为无法计算出参数 (1 2 +) 的值;而宏进行展开得到的 (+ 1 2) 就能够被计算。
三
假设有这样一个宏:
(defmacro twice [e] `(do ~e ~e))
这个宏把 (twice (println "foo")) 展开为 (do (println "foo") (println "foo")),也就是打印了两次 foo。
如果是函数的话……
(defn twice [e] `(do ~e ~e))
调用函数 (twice (println "foo")) 的话,会先计算 (println "foo") 的值,由于 println 没有返回值,所以只会得到 nil。函数体最终执行的代码是 (do nil nil)。
总而言之,宏是用来转换代码,或者说给代码变形的,也就是 Transform;而函数是用来接收参数并执行代码的。
2026-03-06