同相性,指的是,
程序和程序所操作的数据采用了统一编码。
Lisp语言使用了S表达式,
例如,(fn x)
既可以看做是程序,用参数x调用函数fn,
也可以看做是数据,由符号fn和符号x构成的列表。
同相性使得我们,可以像处理数据一样处理代码。
做一些代码转换之类的工作,十分简单。
例如,
当遇到(fn x)时,
我们可以让它先转换成,
(begin
(display x)
(gn x))
然后再执行。
甚至也可以用来定义变量,
(define-with-display (f a)
(g a))
转换成,
(define (f a)
(display a)
(g a))
这种代码层面的转换称为“宏”(macro)。
定义一个宏
Scheme是Lisp的一个简洁方言,
它使用define-syntax来定义宏。
本质上,宏是一个特殊的标识符,
它关联了转换器函数。
表达式的求值过程,分为了3个阶段,
读取期,宏展开期,运行期。
在遇到宏调用的时候,
Scheme会先调用与之关联的转换器,进行代码转换,(宏展开期)
然后再求值结果表达式。(运行期)
在解释器中,宏展开和表达式求值可能是交替进行的,
而在编译器中,他们是两个独立的阶段。
(define-syntax or
(syntax-rules ()
[(_) #f]
[(_ e) e]
[(_ e1 e2 e3 ...)
(let ([t e1]) (if t t (or e2 e3 ...)))]))
以上代码定义了一个宏,or,
它用来对(or ...)表达式进行变换。
(or)转换成了#f
(or a)转换成了a
(or a b)转换成了(let ([t a]) (if t t (or b)))
我们看到,
宏展开是支持递归调用的。
模式匹配
syntax-rules使用了模式匹配来定义转换器,
它的每一条语句给定了形如“[模式 模板]”的转换规则,
如果模式匹配成功了,
就按着模板的方式进行转换。
[(_ e) e]
其中,
模式是(_ e),
模板是e,
“_”表示通配符。
这个模式匹配了(or e),
转换结果为e,
即它能把(or a)转换成a。
我们再来看(_ e1 e2 e3 ...),
其中的省略号“...”,
并不是为了演示方便故意省略了。
“...”是一个标识符,是模式匹配的一部分,
它用来代表“和前面一样的匹配”。
模板中也出现了“...”,
它会根据模式中“...”来进行填充。
Scheme中使用的模式匹配,是一个庞大的主题,
甚至模式匹配已经构成了一门新的语言,
TSPL4中进行了详细的解释,Syntax-Rules Transformers
转换器函数
另外一种定义宏的方式是,
显式的指定宏展开器函数。
(define-syntax r
(lambda (x)
(display x)
(display "")
#t))
我们用lambda定义了一个匿名函数,
并让它与宏标识符r相关联。
我们直接在REPL中看看r是什么,
#<syntax r>
#t
第一行是(display x)副作用,
可见x的值是#<syntax r>,称为语法对象(syntax object)。
然后r被转换成#t,
第二行是REPL中打印了#t的值。
为了处理转换器中匹配到的语法对象,
Scheme语言提供了syntax-case特殊形式。
(define-syntax or
(lambda (x)
(syntax-case x ()
[(_) #'#f]
[(_ e) #'e]
[(_ e1 e2 e3 ...)
#'(let ([t e1]) (if t t (or e2 e3 ...)))])))
它使用了与syntax-rules相同的模式匹配规则,
不同的是,我们还需要显式构造模板中的语法对象。
对于宏调用(or a b)来说,x的值是#<syntax (or a b)>,
syntax-case会先求值x,然后解开语法对象的封装,得到(or a b),
再进行模式匹配。
语法对象
语法对象,包装了标识符的作用域信息。
我们知道Scheme的宏是卫生的(hygienic),
宏展开后的标识符还处在其来源处的词法作用域中,
为了达成这个目的,作用域信息就要被保存起来。
Scheme的不同实现有不同的做法,
Petite Chez Scheme使用了语法对象进行封装。
语法对象由syntax特殊形式创建,(syntax e)
#'e是它的简写,
在程序的读取阶段会被展开为(syntax e)。
前文我们说,
“模式匹配构成了一门新的语言”,并不为过,
因为#'有很多规则(坑)需要我们了解。
(1)出现在“模式”中的变量,称为模式变量(pattern variable),
模式变量的值是它匹配的值。
例如:(_ a b)匹配(or x y),a和b就是模式变量,a的值是x,b的值是y
(2)#'e的值是一个语法对象,e可以是模式变量也可以不是。
如果e是模式变量,则值为#<syntax e匹配的值>,
如果e不是模式变量,则值为#<syntax e>。
(3)“模板”中的模式变量,必须出现在#'或者#'(...)中,不能裸写。
Pattern variables, however, can be referenced only within syntax expressions
(4)#'(a b)不是一个语法对象,而是由语法对象构成的列表,(#'a #'b)
例如:[(_ a) #'(a b)],结果是(#<syntax a匹配的值> #<syntax b>)
注意到b不是模式变量。
(5)多层#',读取器会先将每一层展开成(syntax ...)再求值。
例如:#'#'a实际上是(syntax (syntax a)),
求值为(#<syntax syntax> (#<syntax syntax> #<syntax a匹配的值>))。
注意到syntax不是模式变量。
可以定义宏的宏
syntax-rules是用来定义宏的,
然而,它也是一个宏,它最终被展开为syntax-case。
(define-syntax syntax-rules
(lambda (x)
(syntax-case x ()
[(_ (i ...) ((keyword . pattern) template) ...)
#'(lambda (x)
(syntax-case x (i ...)
[(_ . pattern) #'template] ...))])))
syntax-rules的目的,
是为了避免显式的书写lambda和#'。
像这种生成syntax-case的宏还有很多,
例如,with-syntax。
(define-syntax with-syntax
(lambda (x)
(syntax-case x ()
[(_ ((p e) ...) b1 b2 ...)
#'(syntax-case (list e ...) ()
[(p ...) (let () b1 b2 ...)])])))
with-syntax的目的,
是把匹配条件写在一起,
最后输出到一个模板中。
从这里我们可以看到,
syntax-case第一个参数的值,
可以是语法对象的列表。
syntax-case会对列表中的语法对象,
解除#<syntax ...>的封装,
然后再进行模式匹配。
结语
Lisp的宏非常强大,
很多人只是听说过,
没有切身使用过,
隐约觉得宏可以解决任何问题。
其实不然,
Lisp宏只是做了一些代码的变换,
简化了已完成功能的描述方式。
本文对Scheme宏的定义和使用做了简单介绍,
希望能揭开它的神秘面纱。