[Emacs] Emacs之魂(六):宏与元编程

数据和代码

如果说Lisp语言有一个特性最能使人津津乐道的话,我想应该是它的宏系统(macro system)了吧,
在Lisp语言中,程序和代码的表现形式(textual representation)几乎一致,造就了它无与伦比的元编程能力。
这种对称性,使得Lisp语言可以像处理数据一样优雅的处理代码本身。

并且和其他语言不同的是,Lisp的宏系统,并不是简单的文本操作,
而是建立在语法对象(syntax object)基础之上。

前文提到过,我们直接写(foo bar bar)表示函数调用或者宏调用(macro call),
加引用'(foo bar bar)表示列表字面量,
直接写x表示变量或者函数,加引用'x表示符号(symbol)。

如果我们把列表字面量和符号看做数据,把变量和函数调用看做程序,
那么数据和程序的表现形式(textual representation)几乎是相同的,只差一个引用。
所以,如果一个函数能够处理数据(列表/变量),那么它也一定能够处理被引用的程序,
同理,如果一个函数能够返回一段数据(列表/变量),那么去掉引用之后(使用eval),
也可以看做它是返回了一段程序。

例如,

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

(inc 'x)    ; (setq x (1+ x))

我们定义了一个inc函数,它接受var作为参数,返回了一个列表。
即,(inc 'x)的求值结果为(setq x (1+ x))
其中,(setq x (1+ x))是一个列表。

我们可以通过eval直接把返回的列表当做程序来执行,

(defvar x 0)
(eval (inc 'x))

x    ; 1

x的值被修改了,变成了1

定义一个宏

我们只需要将上文的inc稍作修改,就可以把它转换成一个宏(macro),
我们只需要将defun改成defmacro即可,

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

现在inc就是一个宏(macro)了,它的使用方式和函数非常相似,

(defvar x 0)
(inc x)

x    ; 1

我们看到,这里直接使用了(inc x),而不是(inc 'x)
并且,(inc x)的作用和直接写程序(setq x (1+ x))是一样的。

(inc x)我们称之为宏调用(macro call),
(setq x (1+ x))我们称之为宏展开(macro expansion)后的程序。

编译器或者解释器会采用不同的策略进行宏展开,
一般而言,编译器会在求值程序之前,将代码中所有的宏(macro)进行展开,
即,将所有的宏调用(inc x),替换成它返回的那段程序(setq x (1+ x))
直到代码中不再包含宏(macro)为止,然后再进行编译。

一个简单的解释器实现,可能会一边执行程序一边进行宏展开操作,
它会在运行时,通过判断符号(symbol)的类型,来决定进行函数调用还是宏调用。
这样可能会有助于理解宏的递归展开问题。

一个宏展开式中,可能还会包含其它的宏,也可能还会包含另一个宏的定义。
(以后的文章中,我们会介绍)

因此,在宏定义中,进行的具有副作用(side effect)的操作,
其执行时机并不是在运行时,而是在宏展开阶段,
而如果宏实参中包含了带有副作用的操作,那么它可能被展开到源代码中的多个位置,
从而被执行多次。

语法对象

在Emacs Lisp中,宏变量inc实际上是一个转换函数,
它将var转换成了(list 'setq var (list '1+ var)),即把符号(symbol)转换成了一个列表对象。

宏变量的值与函数一样会保存在符号(symbol)inc的function cell中,
因此,一个符号(symbol)不可能既表示一个函数又表示一个宏(macro)。

当Lisp解释器遇到一个符号(symbol)的时候,
会判断它到底是一个变量,一个函数还是一个宏(macro)。

(defun add1 (x)
    (+ x 1))

(defvar a 1)
(add1 a)

如果是一个函数,且当前进行的是函数调用(add1 a)
那么就会先求值它的实参,a求值为1
再将add1的形参x绑定为实参的值1,再求值函数体,
即,求值(+ x 1),结果为2

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

(defvar x 0)
(inc x)
x    ; 1

如果是一个宏(macro),且当前进行的是宏调用(inc x)
那么它并不会像函数那样先求值函数体,而是直接将宏形参绑定为宏调用的实参值。
即,var绑定为符号(symbol)x

值得注意的是,宏调用的实参,是一个符号(symbol),它是一个Lisp对象,而不是一个字符串,
宏(macro)所返回的结果,也是一个Lisp对象。

更明确的说,宏(macro)是一个针对语法对象(syntax object)的变换函数,
它对读取器获得的语法对象(syntax object)进行变换。
在某些Lisp方言,例如Scheme,这些语法对象(syntax object)包含了上下文信息,使用它们可以编写出强大而灵活的宏(macro)。

这里容易引起混乱的是,在Emacs Lisp中,直接使用了符号和列表表示了语法对象,
而实际上语法对象是一个数据结构,在其内部包含了符号和列表的信息。
这样做的好处是,在宏展开阶段宏(macro)接受和返回的都是语法对象,
而在运行时阶段,处理的都是运行时对象了。
(例如:syntax->datum和datum->syntax

通过以下程序我们可以验证,var确实是一个符号(symbol)。

(defmacro inc (var)
    (message "%s" (symbolp var))    ; t
    (list 'setq var (list '1+ var)))

我们之前十分小心的区分了标识符,符号(symbol)和变量,
是为了在类似这样的场景中保持清醒。

标识符经过Lisp读取器,在Lisp内部会变成一个符号(symbol),它是一个语法对象,
然后Lisp会对所有的宏(macro)进行展开,将这些语法对象绑定到宏形参上,对语法对象进行变换。
最后,求值器在运行时会求值这些符号(symbol),得到一个变量值或者函数值。

因此,编写宏(macro)可以看作是对编译器或者解释器进行编程,
Lisp允许用户在表达式被求值之前对它进行一些变换。

总结

本文初步介绍了Lisp的宏系统,展示了宏调用与函数调用之间的异同,
我们发现Lisp的宏系统是建立在语法对象(syntax object)基础之上的,而不是简单的进行文本替换。
此外,由于Emacs Lisp的宏(macro)不是卫生的(hygienic),所以会和Common Lisp一样出现变量捕获问题。
下文我们开始介绍一些Lisp宏的常见陷阱和用法。

参考

GNU Emacs Lisp Reference Manual
Chez Scheme Version 8 User's Guide
An Introduction to Scheme and its Implementation

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

推荐阅读更多精彩内容