3.1 句法扩展(宏)
就象我们在2.5节看到的一样,let
句法形式本质上是通过lambda
表达式及过程应用定义出来的一个句法扩展,lambda
表达式以及过程应用两者都属于核心语法形式。在这一点上,你可能想知道哪些句法形式是核心语法,哪些是宏,以及怎样定义新的宏。本节提供这些问题的一些答案。
事实上,我们没有必要明显地区分核心语法与宏,因为宏一旦定义,就具有了与核心语法相同的地位。然而这里对它们进行区分,是为了更容易理解语言本身,因为它允许我们将注意力集中在核心语法上,并以此为基础进一步理解其它的方面。
Scheme 实现需要区分核心形式和宏。作为编译或解释执行的第一步,Scheme 实现首先将宏展开成为核心语法形式,接下来只需要关注核心语法形式的解释或编译。语言标准并未定义哪一个语法形式应该实现为核心语法,那一个语法形式应该实现为宏,所以,这里讨论的核心语法是与实现相关的,在不同的实现中可能会与这里描述的不一样。(译注:这一段并未尊重原著,我觉得用宏来代替句法扩展并无不妥,还更好理解)
语言的核心语法里具体应该包含哪些东西是有争议的,应该足以通过核心语法扩展出其它所有的语法形式。这里讨论的集合是满足这一约束条件的最简单的集合。(也就是说,一整个完整的语言都可以通过这里描述的这个小子集实现出来)
核心语法里包括顶级定义define
,常量,变量,过程应用(函数调用),quote
表达式,lambda
表达式,if
表达式, 以及set!
表达式。下面的语法表描述了这些定义和表达式。在语法表里,竖线(|)表示单选,一个语法形式后面跟着星号(*)表示该形式的零个或多个。<variable>
可以是任何 Scheme 标识符。<datum>
可以是任何 Scheme 对象,例如一个数字,一个列表,一个符号或者一个向量。<boolean>
是 #t 或 #f 之一,<number>
可以是任何数字,<character>
可以是任意字符,<string>
可以是任意字符串。我们已经见过数字,字符串,列表,符号以及布尔值的例子了。有关这些对象和其他对象的对象级语法的更多信息,请参阅第六章或语法描述。
<program> --> <form>*
<form> --> <definition> | <expression>
<definition> --> <variable definition> | (begin <definition>*)
<variable definition> --> (define <variable> <expression>)
<expression> --> <constant>
| <variable>
| (quote <datum>)
| (lambda <formals> <expression> <expression>*)
| (if <expression> <expression> <expression>)
| (set! <variable> <expression>)
| <application>
<constant> --> <boolean> | <number> | <character> | <string>
<formals> --> <variable>
| (<variable>*)
| (<variable> <variable>* . <variable>)
<application> --> (<expression> <expression>*)
上面的语法表其实是含糊不清的,因为过程应用的语法与 quote
, lambda
, if
, 以及 set!
表达式相冲突。为了有资格作为一个过程(函数)被调用,至少在最先的<expression>
里,不能包含 quote,lambda, if, 以及 set! 等关键字。除非这些关键字已经被重新定义过,或者处于一个局部绑定中。
[译注]这里不是很理解,lambda 表达式应该是不冲突的((lambda (形参) ...) 实参)
显然是合法的过程调用,if
也有可能是合法的,它可以根据条件返回不同的函数对象嘛。quote
表达式倒是真的冲突了,((quote ...) arg)
显示不是合法的表达式. set!
似乎也冲突,set! 的返回值好象是未定义,那就不能把 set! 表达式放在括号后面当作函数来调用了。
2.6节给出的defun
语法并未包含在核心语法当中(那是 Lisp 的语法),类似地,if
语法不允许省略 else 子句,但是当一个 if 表达式缺失了 else 子句的时候,只要用任意的表达式(比如 #f)来顶替缺失的子句,就能转换为符合核心语法规定的 if 表达式. [译注]在这一点上,几乎所有的 Scheme 实现都允许省略 else 子表达式,倒是 Racket 这个不是 Scheme 的 Scheme,它的 else 子表达式是不能省略的。
只包含定义(<definition>
)的begin
表达式被认为是语法定义,这是合法的,使得一个宏可以展开成多个定义(不知道对不对)。begin
表达不是核心语法的一部分,以下面的表达式为例:
(begin e1 e2 ...)
它等价于
((lambda () e1 e2 ...))
begin
表达式可以转化为 lambda
表达式,所以它没有必要包含在核心语法中。
现在,我们已经建立起一组核心语法,让我们来讨论扩展语法(宏)。宏之所以被称作“扩展语法”就是因为它将 Scheme 的语法扩展到了核心语法之外。Scheme 的所有扩展语法都必须最终从核心语法中导出。(也就是说,一个宏定义中可以引用另一个宏,但是当一个宏展开后到最后应该只剩下核心语法形式,否则没办法解释执行了,此处删除了一段难以理解的原文)。
语法扩展通过define-syntax
来完成,define-syntax
和 define
类似,不同之处是,define-syntax
将语法变换程序与一个语法关键字(比如 let)建立绑定,而 define
将一个变量和值相绑定。
(define-syntax let
(syntax-rules ()
[(_ ((x e) ...) b1 b2 ...)
((lambda (x ...) b1 b2 ...) e ...)]))
紧跟在 define-syntax 后面的标识符就是被定义的扩展语法的名字,或者关键字,在上例中是 let
。syntax-rules
表达式的求值结果是一个变换程序。跟在 syntax-rules
后面的列表被称为 辅助关键字 , 通常情况下是个空表 ()。需要辅助关键的一个例子是cond
表达式的 else 子句。(其它需要使用辅助关键字的例子在第八章中给出)。跟在辅助关键字后面的是一个 规则 序列,其中包含一个或多个规则(或者被称之为 模式/模板 对)。在我们的 let
例子中只有一个规则。每一条规则的 模式 部分指定了输入的形式,而 模板 部分指定了如何对对输入的形式做变换。
模式应该始终是一个结构化表达式,其第一个元素是下划线(_)。(我们将在第八章中看到,下划线的使用只是一个约定,但它是一个很好的例子)如果存在多个规则,则输入表达式会在宏展开期间通过模式匹配来选择适当的规则,如果没有匹配任何规则,则报出“语法违规”。
模式中除了下划线和省略号以外的标识符全都是模式变量,除非它们被列为辅助关键字。模式变量匹配任何子结构,并且绑定到模板中对应的子结构上面。模式中的省略号(...)允许匹配输入原型中的零个或多个表达式。与此类型,模板中的expr ...
从省略号原型表达式中生成零个或多个表达式。输入中的表达式数量决定了输出中的表达式数量;模板中的任何省略号原型必须包含至少一个模式中的省略号原型的模式变量。(如果你看不懂不是我的错,是Google的错, 尝试着看代码吧,代码比文字好懂)
在let
的定义中的单一规则应该是不言自明的,但是有几点值得提一下。首先,let
的语法要求body部分至少要包含一个表达式;因此,我们指定 b1 b2 ...
而不是 b ...
, ([译注]此处有巨坑,...
表示匹配前面一个表达式0到多次,而不是1到多次。所以, b1 b2 ...
表示这里至少要有一个表达式匹配b1
,而b2
是可有可无的,而b ...
则表示0到多个表达式,甚至可以没有表达式。把它当成正则表达式就容易理解了。)另一方面,如果我们不要求这里必须有一个表达式时,可以简单地使用(x e) ...
; 其次,模式变量 x
和 e
在模型中是在一起的,但是在模板中它们是分开的, 可以任意地重排模式变量的顺序。最后,在模式中出现在省略号原型中的三个模式变量x
, e
和b2
也出现在了模板的省略号原型中。这不是巧合,而是一个要求。通常,如果模式变量出现在模式中的省略号原型中,则它不能出现在模板中省略号以外的地方。[再注]严格地说,这里有两个省略号原型,其中一个是(x e) ...
,而另一个是 b2 ...
,所以,b1
并不在省略号原型中。
下面的定义比let
更复杂一些
(define-syntax and
(syntax-rules ()
[(_) #t]
[(_ e) e]
[(_ e1 e2 e3 ...)
(if e1 (and e2 e3 ...) #f)]))
这个定义是递归的,并且涉及到多个规则。单独调用 (and)
总是返回#t
,第一条规则对应的就是这一情况。第二和第三条规则指定递归的基本情况和递归步骤,并将两个或多个子表达式转换为嵌套的if
表达式。例如 (and a b c)
首先展开成:
(if a (and b c) #f)
然后
(if a (if b (and c) #f) #f)
最后
(if a (if b c #f) #f)
展开到最后,如果 a 和 b 求值为真,则该表达式的值为 c, 否则 #f 就是整个表达式的值。
下面的 and
版本更简单,不幸的是它是错的
(define-syntax and ; incorrect!
(syntax-rules ()
[(_) #t]
[(_ e1 e2 ...)
(if e1 (and e2 ...) #f)]))
下面的表达式
(and (not (= x 0)) (/ 1 x))
如果 x
不为 0 ,则返回 (/ 1 x)
. 然而,如果利用不正确的版本来展开它的话:
(if (not (= x 0)) (and (/ 1 x)) #f) -->
(if (not (= x 0)) (if (/ 1 x) (and) #f) #f) -->
(if (not (= x 0)) (if (/ 1 x) #t #f) #f)
当 x
不等于 0, 最后的结果是 #t
,而不是(/ 1 x)
。
下面的 or
的定义类似于 and
,不同之处是必须将每个中间值存入临时变量t
中,以便我们可以测试该值,并且当该值为真时将其返回。(and
不需要临时变量,因为在 Scheme 只有一个布尔值表示假,那就是 #f)
(define-syntax or
(syntax-rules ()
[(_) #f]
[(_ e) e]
[(_ e1 e2 e3 ...)
(let ([t e1])
(if t t (or e2 e3 ...)))]))
和 lambda
以及 let
绑定的变量类似,由模板引入的标识符也是词法作用域,即,仅仅在模板引入的表达式中可见。因此,即使表达式e2 e3 ...
中的某个模式变量引用了t
,新引入的绑定也不会“捕获”那些引用。这通常通过引入的标识符自动重命名来实现。[译注]这一段不好说明白,大意是,递归开始,t
被绑定为e1
,进入下一层递归,t
又被绑定为e2
,此时,外层的绑定不会干扰到内层的绑定,不会搞出 e2 = t = e1
这种乌龙。这就是所谓的卫生宏吧。要注意的是,这是宏,不是函数。但是它的作用域规则与函数是一样的。在内部实现上,通常是通过标识符自动重命名来避免冲突。
与上述and
的简单版本一样,or
的简单版本也是不正确的
(define-syntax or ; incorrect!
(syntax-rules ()
[(_) #f]
[(_ e1 e2 ...)
(let ([t e1])
(if t t (or e2 ...)))]))
原因比较微妙,作为一个练习 3.2.6。
练习 3.1.1 将下面的表达式展开成基本的核心语法形式
(let ([x (memv 'a ls)])
(and x (memv 'b x)))
练习 3.1.2 将下面的表达式展开成基本的核心语法形式
(or (memv x '(a b c)) (list x))
练习 3.1.3 let*
和let
比较相似,但是它是按顺序求值并绑定。每一个右侧的表达式都处在早期绑定的作用域范围内,也就是说,后面的绑定可以引用前面的绑定的值。
(let* ([a 5] [b (+ a a)] [c (+ a b)])
(list a b c) => (5 10 15)
let*
可以用嵌套的let
表达式来实现,例如,上面的let*
表达式与下面的嵌套的let
表达式是等价的
(let ([a 5])
(let ([b (+ a a)])
(let ([c (+ a b)])
(list a b c)))) => (5 10 15)
请使用 define-syntax
定义出 let*
。
练习 3.1.4 正如我们在2.9节中看到的那样,省略 if
表达式的第三个子表达式(else 子表达式)是合法的。但是,这样做往往会导致混乱。Scheme 提供了两种语法形式,when
以及 unless
,可以用来代替这种单分支的if
表达式。
(when test expr1 expr2 ...)
(unless test expr1 expr2 ...)
在这两种语法形式中,test
首先被求值,在when
表达式中,如果test
求值为真, 剩下的子表达式(expr1 expr2 ...
)将被顺序求值,就好像它们包含在一个隐含的begin
表达式中一样。如果test
求值为假,则剩下的子表达式不会被求值,返回值为“未定义”。在 unless
表达式中正好相反,子表达式只有在 test
求值为假的情况下才会被求值。
(let ([x 3])
(unless (= x 0) (set! x (+ x 1)))
(when (= x 4) (set! x (* x 2)))
x) => 8
使用if
和begin
定义出when
,然后再用 when
定义出unless
。