Emacs之魂(七):变量捕获与卫生宏

回顾

上文我们介绍了宏,它与函数是不同的,函数调用发生在程序执行期间,函数在调用之前,会先对它所有的实参进行求值,然后将形参绑定到这些实参的求值结果上,函数的返回值会作为函数调用表达式的值,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 ...

其中,docdeclareinteractive都是可选的。

交互函数的定义中,具有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 kC-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,而不是57
卫生宏是一种语言特性,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


下一篇:Emacs之魂(八):反引用与嵌套反引用

文章被以下专栏收录