回顾
上文我们介绍了宏,它与函数是不同的,函数调用发生在程序执行期间,函数在调用之前,会先对它所有的实参进行求值,然后将形参绑定到这些实参的求值结果上,函数的返回值会作为函数调用表达式的值,Lisp求值器不断的求值表达式,从而程序得以运行。
宏调用(macro call)发生在程序的编译期,或者说,宏调用发生在表达式的求值之前,在执行宏调用的过程中,宏形参直接绑定为实参所代表的语法对象(syntax object)上,宏调用的返回值,会进行表达式替换,将宏调用表达式替换为它的返回值,这个过程称为宏展开(macro expansion),之后在运行时,求值器就不会遇到宏了,所进行求值的只有被展开之后的表达式。
1. 交互函数
在介绍常用的宏之前,我们先介绍Emacs中交互函数(interactive function)的概念。
交互函数可以使用M-x
在echo area中通过输入函数名进行调用(交互式调用),所以交互函数也称为命令(command)。
交互函数也可以被Lisp程序中的其他函数直接调用,这种调用方式称为非交互式调用。
Emacs中函数定义defun
包含以下几个部分,
defun name args [doc] [declare] [interactive] body ...
其中,doc
,declare
和interactive
都是可选的。
交互函数的定义中,具有interactive
部分,
它是一个形如(interactive arg-descriptor)
的表达式,用来指定该函数被交互调用时的行为,
对于非交互式调用,interactive
部分将失去作用。
arg-descriptor
有三种可能的写法:省略,一个字符串,或者一个Lisp表达式。
具体情况可能会比较复杂,可以参考Using-Interactive。
1.1 describe-key
describe-key
是一个交互函数,用来展示某个快键键相关的文档信息,
我们可以使用M-x describe-key
来调用它,echo area中会显示如下内容,等待我们键入一个快捷键,
如果我们键入一个快捷键,例如C-a
,Emacs就会展示出与C-a
相关的文档信息了。我们还可以使用快捷键C-h k
,C-h k
相当于M-x describe-key
。
C-a runs the command move-beginning-of-line (found in global-map),
which is an interactive compiled Lisp function in ‘simple.el’.
It is bound to C-a.
(move-beginning-of-line ARG)
Move point to beginning of current line as displayed.
(If there’s an image in the line, this disregards newlines
which are part of the text that the image rests on.)
With argument ARG not nil or 1, move forward ARG - 1 lines first.
If point reaches the beginning or end of buffer, it stops there.
To ignore intangibility, bind ‘inhibit-point-motion-hooks’ to t.
1.2 describe-function
describe-function
也是一个交互函数,用来展示某个函数(或者宏)相关的文档信息,它绑定到了快捷键C-h f
上,
调用后,echo area中会显示如下内容,等待我们输入函数(或者宏)的名字,
例如,when
相关的文档信息如下:
when is a Lisp macro in ‘subr.el’.
(when COND BODY...)
If COND yields non-nil, do BODY, else return nil.
When COND yields non-nil, eval BODY forms sequentially and return
value of last one, or nil if there are none.
它指出,when
是一个宏,并且定义在subr.el
文件中。
鼠标左键点击subr.el
,会打开本地subr.el.gz
文件中when
的定义,如下,
(文件路径为:/Applications/Emacs.app/Contents/Resources/lisp/subr.el.gz
(defmacro when (cond &rest body)
"If COND yields non-nil, do BODY, else return nil.
When COND yields non-nil, eval BODY forms sequentially and return
value of last one, or nil if there are none.
\(fn COND BODY...)"
(declare (indent 1) (debug t))
(list 'if cond (cons 'progn body)))
可见,when
只是一个语法糖,最终会展开成if
表达式。
subr.el.gz
文件中包含了很多常用的宏,
我们可以访问线上地址Github: emacs-mirror/emacs subr.el进行查阅。
2. 变量捕获
2.1 插入一个绑定
; -*- lexical-binding: t -*-
(defmacro insert-binding (x)
`(let ((a 1))
(+ ,x a)))
以上代码定义了一个宏insert-binding
,它将展开成一个let
表达式,
将x
插入到一个a
值为1
的词法环境中。
其中,``(let ((a 1)) (+ ,x a)))`是反引用表达式,
下一篇文章中我们再详细讨论。
(insert-binding 3)
将展开成,
(let ((a 1))
(+ 3 a)) ; 4
然而,如果x
中包含a
,就会引发歧义,例如,
(let ((a 2))
(insert-binding (+ a 3)))
上式会展开为,
(let ((a 2))
(let ((a 1))
(+ (+ a 3) a))) ; 5
我们看表达式(+ (+ a 3) a))
,
其中,左边第一个a
,来源于宏展开之前的词法绑定,即,
(let ((a 2))
(insert-binding (+ a 3)))
而第二个a
,来源于宏展开式中的词法绑定,
`(let ((a 1))
(+ ,x a))
在进行宏定义时,我们并不知道x
中有没有a
,
结果导致了,宏展开式中的词法绑定意外捕获了x
中的a
。
在本例中,x
就是(+ a 3)
,其中a
的值本来应该是2
,
结果展开后,被宏展开式所捕获,值变成了1
,
我们通过插入一个词法绑定,完成了本例。
2.2 插入一个自由变量
; -*- lexical-binding: t -*-
(let ((a 1))
(defmacro insert-free (x)
`(+ ,x a)))
以上代码定义了一个宏insert-free
。
(insert-free 3)
将展开为(+ 3 a)
,其中a
是自由变量,
a
的值取决于(insert-free 3)
在何处被展开。
例如,
(let ((a 2))
(insert-free (+ a 3)))
将展开为,
(let ((a 2))
(+ (+ a 3) a)) ; 7
我们再来看表达式(+ (+ a 3) a))
,
其中,左边第一个a
,来源于宏展开之前的词法绑定,即,
(let ((a 2))
(insert-free (+ a 3)))
而第二个a
,来源于宏展开式中的词法绑定,
(let ((a 1))
(defmacro insert-free (x)
`(+ ,x a)))
在进行宏定义时,虽然我们显式的将a
绑定为1
,
但是x
中包含的绑定,意外影响到了它,使得a
的值变成了2
。
我们通过插入一个含自由变量的表达式,让它受展开式所处的位置影响。
2.3 hygienic macro
以上两个例子中,插入一个绑定会污染宏展开后的环境,而插入一个自由变量会被宏展开后环境所影响,
它们都有变量捕获问题,都不是卫生的(hygienic)。
hygienic macro通常翻译成“卫生宏”,是一种避免变量捕获的技术,
如果所使用的宏是卫生的,那么以上两个例子中,最后的求值结果应该都是6
,而不是5
和7
。
卫生宏是一种语言特性,Scheme中的宏是卫生的,而Emacs Lisp不是。
如果一个宏是卫生的,
那么宏展开式中的所有标识符,仍处于其来源处的词法作用域中。
(1)例如,根据insert-binding
的定义,
; -*- lexical-binding: t -*-
(let ((a 1))
(defmacro insert-free (x)
`(+ ,x a)))
(let ((a 2))
(insert-binding (+ a 3)))
将展开为,
(let ((a 2))
(let ((a 1))
(+ (+ a 3) a)))
其中,(+ (+ a 3) a)
中,
第一个a
,来源于宏展开之前的词法环境,这个a
的值为2
,
第二个a
,来源于宏定义式,这个a
的值为1
,
因此,(+ (+ a 3) a)
求值为6
。
(2)又例,根据insert-free
的定义,
; -*- lexical-binding: t -*-
(let ((a 1))
(defmacro insert-free (x)
`(+ ,x a)))
(let ((a 2))
(insert-free (+ a 3)))
将展开为,
(let ((a 2))
(+ (+ a 3) a))
同理,(+ (+ a 3) a)
中,
第一个a
,来源于宏展开之前的词法环境,这个a
的值为2
,
第二个a
,来源于宏定义式,这个a
的值为1
,
因此,(+ (+ a 3) a)
的值也为6
。
总结
本文介绍了交互函数,介绍了如何查看一个函数或者宏的文档和定义,
一些常用的宏,都可以通过查看subr.el
来找到它们。
然后,我们介绍了两种与宏相关的变量捕获问题,引出了卫生宏的概念。
下文,我们继续讨论宏,来看一看展开为宏定义的宏之强大威力。
参考
GNU Emacs Lisp Reference Manual
On Lisp
Let Over Lambda
The Scheme Programming Language