Paul的书把lisp的宏夸到了无以附加的地步,但是作为一本闲聊的书它并没有深入介绍lisp的宏,甚至连基本概念都没提到。这对于学习lisp的人非常有害——它会造成一种莫名的崇拜,并且这种崇拜还是对于未知的崇拜,读者根本没有理解任何东西。这样的崇拜让我在读第一本lisp书时就找宏看,结果在没有理解其他部分时就接触到宏,当时就看得一头雾水。而且一些强大的东西往往也会有很麻烦的副作用,宏也一样,但是Paul并没有把这些问题讲明白,其实在一些其他正经介绍lisp的书中都是非常小心地强调宏的危害——良好定义的宏会有非常强大的力量,但是一些垃圾宏会让整个程序变得难以理解并且混乱不堪。
其实我看懂宏是由于这篇文章,这文章没有任何对宏的赞扬,但是一旦你读懂你就会发现宏真正伟大的地方,它是从实用的角度来介绍宏,这种实用就是把宏当成代码生成器,并且文章还将宏与编译器以及一些其他技术进行对比来突出宏的优点。宏的基本概念在那篇文章中已经说得很清楚了,在这里就不罗嗦了。在这里我就只说一下为什么宏在scheme中非常好写,以及它的危害。
scheme的宏的实现
几乎所有的scheme实现都支持R5RS标准,这个标准定义了syntax-rules宏,在R6RS标准中定义了syntax-case宏,但是由于几乎所有的主流scheme实现都没有支持R6RS,很多也没有支持的打算,所以我就没去学。相对于Common Lisp的宏scheme的是相当的高阶,scheme的syntax-rules基于的是Patterns/Templates,也就是说在scheme中写宏只需要定义宏匹配的模式以及模式对应的模板就能实现干净的宏了。但是Common Lisp却没有这实现,只能用更加底层的宏。具体syntax-rules用法可以看这本书,这里也不说了。
宏的匹配
根据前面那篇文章,宏本身是用来生成代码的,这样说就是把编写宏和编写编译器进行比较了,只是宏更加简单,因为lisp整个语言表示就是基于链表的,而链表可以实现更加复杂的树型结构,这就类似于编译原理常常提到的语法树,是的,这也就是为什么Paul说写lisp程序就是直接写语法树了,而scheme宏的Patterns匹配的很大程度上就依赖于这一点。
使用syntax-rules可以让我们非常好地理解lisp相对于其他语言的优势:假设我们要实现and的短路效应,我们可以按照下面的方法进行定义:
(define-syntax and (syntax-rules () ((and e1 e2) (if e1 e2 #f))))
在定义了and宏之后我们用如下方法使用:
(and (not (= x 0)) (/ 1 x))
这是一个经典的避免被除数为0的方法,我们直接匹配了and宏,然后宏帮我们扩展成原始的语法。其中e1匹配了(not (= x 0)),e2匹配了(/ 1 x),之后宏再将and表达式扩展成(if (not (= x 0)) (/ 1 x) #f),这样前面对于and宏的调用就能按照预计的去执行。这一切最值得惊奇的是宏竟然能恰如预想的让e1匹配(not (= x 0)),而不是(或not等,这是因为你在写这代码时已经用括号指明了(not (= x 0))是一个表达式,他们是不能再分的了,假如lisp没有这些括号那么这样表达力超强的宏也就没有了用武之地。用任何其他语言都无法实现这样高效的宏。
为什么要小心宏?
lisp最为核心的一些函数就是lambda,car,cdr,eval,apply等,这些函数都是非常简单的,如果只有这些那么lisp是真正的没有任何语法了。这些基本的函数以及用户定义的函数运行方式由下面两个规则定义,这就是eval函数遵循的规则:
- 先解析表达式的子表达式。
- 整个表达式最左边子表达式的值就是操作符,其他子表达式的值就是操作数,将操作数作为参数调用操作符。
所以对于下面的表达式:
(+ (- 3 1) (* 2 3) 2)
lisp是先eval(- 3 1)和(* 2 3),再将这些表达式的返回值当作参数传给+函数。
在你写上面的表达式时你根本不需要考虑任何语法,甚至连算符的优先级都不需要考虑!所以说lisp是几乎没有语法的,正是由于这一点lisp可以说是非常容易学习的语言,你不需要像学其他语言一样去记住所有的语法,对于基本函数的调用规则只需要记住eval函数的两个规则就行。在开发软件时也一样,只使用最核心的函数可以让整个软件变得容易理解。宏一般是作为扩展lisp的语法而存在的,也就是说你定义一个宏就相当于定义了一个语法,这些语法会增加整个程序的复杂度,让软件变得更加难以理解。所以对于大型的软件要尽量少用宏,如果能用函数尽量用函数,不要用宏。