14.2 宏调用的展开

宏调用在形式上与函数调用完全相同:都是以宏的名称开头的列表,列表中的其余元素即为宏的参数。

宏调用的求值过程与函数调用类似,但有一个关键区别:宏的参数就是宏调用中直接写出的表达式本身,在传递给宏定义之前不会被求值。与之相对,函数的参数是函数调用列表中各元素求值后的结果。

获取参数后,Lisp 会像调用函数一样执行宏定义。宏的参数变量会绑定到宏调用中的参数(若是 &rest 参数,则绑定到参数列表),宏体的执行与返回值的方式也与函数体一致。

宏与函数的第二个关键区别在于:宏体返回的值是另一段 Lisp 表达式,也就是该宏的 展开式(expansion)。Lisp 解释器在从宏得到这段展开式后,会立刻对其进行正常求值。

由于展开式会按常规方式被求值,因此它内部可以包含对其他宏的调用,甚至可以是对同一个宏的递归调用(尽管这种情况并不常见)。

需要说明的是,Emacs 在加载未编译的Lisp 文件时会尝试展开宏。这一操作并非总能实现,但如果成功,会提升后续代码的执行速度。See 程序的加载方式

你可以通过调用 macroexpand 函数来查看指定宏调用的展开式。

Function: macroexpand form &optional environment

form 是一个宏调用,该函数会对其进行展开。如果展开结果仍是另一个宏调用,则会继续展开,直到得到一个非宏调用的表达式为止。这个最终表达式即为 macroexpand 的返回值。若 form本身并非宏调用,则会按原样返回。

需要注意的是,macroexpand 不会处理 form 的子表达式(不过部分宏定义可能会主动处理)。即便这些子表达式本身是宏调用,macroexpand 也不会对其展开。

macroexpand 函数不会展开对内联函数的调用。通常也无需这么做,因为内联函数调用的可读性与普通函数调用无异。

若提供了 environment 参数,它会指定一个宏定义关联列表(alist),该列表中的宏定义会覆盖当前已定义的宏。字节编译功能会用到这一特性。

(defmacro inc (var)
    (list 'setq var (list '1+ var)))

(macroexpand '(inc r))
     ⇒ (setq r (1+ r))

(defmacro inc2 (var1 var2)
    (list 'progn (list 'inc var1) (list 'inc var2)))

(macroexpand '(inc2 r s))
     ⇒ (progn (inc r) (inc s))  ; inc not expanded here.
Function: macroexpand-all form &optional environment

macroexpand-allmacroexpand 一样会展开宏,但它会查找并展开 form 中所有的宏(而非仅展开顶层宏)。如果没有任何宏被展开,返回值会与 form 满足 eq 相等性。

沿用前文 macroexpand 的示例,改用 macroexpand-all 执行时可以看到:macroexpand-all 确实会展开内嵌的 inc 调用:

(macroexpand-all '(inc2 r s))
     ⇒ (progn (setq r (1+ r)) (setq s (1+ s)))
Function: macroexpand-1 form &optional environment

该函数与 macroexpand 一样会展开宏,但仅执行一步展开操作:若展开结果仍是另一个宏调用,macroexpand-1 不会继续展开它。


emacs

Emacs

org-mode

Orgmode

Donations

打赏

Copyright

© 2025 Jasper Hsu

Creative Commons

Creative Commons

Attribute

Attribute

Noncommercial

Noncommercial

Share Alike

Share Alike