数据和代码
如果说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