Skip to content

LISP - 函数与宏

Lisp 的超能力:函数(Functions) vs. 宏(Macros)

Section titled “Lisp 的超能力:函数(Functions) vs. 宏(Macros)”

理解函数(function)和宏(macro)之间的区别是掌握 Common Lisp 的基础。尽管它们看起来相似,但它们在程序执行的不同阶段运行。函数在**运行时(runtime)处理数据,而宏在编译时(compile time)**转换代码。这种元编程(metaprogramming)能力正是 Lisp 如此可扩展的原因。

函数是大多数语言中用于计算的标准构建块。

  • 求值:参数首先被求值,然后它们的求值结果作为值传递给函数体。
  • 操作对象:值(数字、字符串、列表等)。
  • 目的:执行计算并返回一个值。
  • 类比:一位遵循食谱的厨师。食材(参数)在烹饪(函数执行)开始之前就被称量和准备好了。

宏是编写其他代码的代码片段。

  • 求值:宏在编译时运行。它接收传递给它的字面代码作为参数。
  • 操作对象:代码(S-表达式)。
  • 目的:将一段代码转换成另一段代码,然后这段代码再被编译和执行。
  • 类比:一位食谱编写者。给定一个像“一道简单的意大利面”这样的请求,他们会编写一个新的、具体的食谱(宏展开),然后厨师将遵循这个食谱。

最关键的区别在于,宏能够控制其参数是否、何时以及求值多少次。函数则没有这种控制——它们的参数总是在函数被调用之前精确地求值一次。

让我们尝试实现自己的 if 语句。使用函数,这是不可能的:

;; The functional approach FAILS
(defun my-if-function (condition then-form else-form)
(if condition then-form else-form))
;; This call will ERROR!
;; Lisp evaluates ALL arguments *before* calling my-if-function.
;; So, (error "I am always called!") runs no matter what.
(my-if-function t (print "Success!") (error "I am always called!"))

然而,宏可以解决这个问题,因为它能生成只包含应该执行分支的代码。

;; The macro approach WORKS
(defmacro my-if-macro (condition then-form else-form)
;; The backquote ` creates a code template.
;; The comma , evaluates and splices in the value.
`(if ,condition
,then-form
,else-form))
;; This call works perfectly!
;; The macro expands into `(if t (print "Success!") (error "...not included"))`
;; The error form is never part of the final code, so it is never evaluated.
(my-if-macro t (print "Success!") (error "I am never called!"))

你可以使用 macroexpand-1 来查看宏生成的代码:

CL-USER> (macroexpand-1 '(my-if-macro t (print "Success!") (error "I am never called!")))
(IF T (PRINT "Success!") (ERROR "I am never called!")) ; The expanded code
T ; Indicates it was a macro expansion

“如果可以用函数实现,就用函数实现。”

宏更复杂,并且可能引入难以察觉的 bug(例如多次求值一个参数)。仅在真正必要的情况下才使用它们:

  • 1. 创建新的控制结构:当你需要控制参数的求值时(例如,if、when、unless、loop)。
  • 2. 减少样板代码:当你有一个重复的代码模式,可以在编译时抽象掉时(例如,with-open-file)。
  • 3. 构建领域特定语言(DSLs):在 Lisp 中为特定问题领域创建新的、专门的语法(例如,一个 HTML 生成库)。
特性函数宏
求值时间运行时编译时
参数处理在执行之前求值参数接收未求值的代码(S-表达式)作为参数
主要目的处理数据进行计算转换和生成代码
核心要点用于所有标准计算用于扩展 Lisp 语法并控制求值