[Emacs] 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容