macro of scheme
intro
大家对宏(特指C/C++的宏)应该不陌生, 甚至被这个东西坑过无数次, 究其原因, C/C++的宏本质上只是文本替换而已, 没有任何语法层面的信息, 因而也就做不到类型检查语法检查这些事情; 而scheme的宏是真正工作在语法树上的, 甚至可以对其进行修改!
define-syntax
在R5RS以前, scheme通过define-macro
来定义宏, 形式如下:
|
|
行为上其实和C/C++的宏是差不多的, 在transform阶段简单的做了token的替换, 看一个简单的例子:
|
|
这个坑很明显, 和C/C++一样, 宏内部出现的名字tmp
被实际使用宏的代码污染了. 不过聪明的scheme实现会提供一个gensym
方法, 用来生成一个独一无二的名字, 好我们试试看改进版本:
|
|
虽然通过gensym
解决了临时变量的名字问题, 但是依然没办法阻止用户污染其他名字, 比如重新定义if
..于是我们意识到:
- 名字(binding)在上下文环境里的重要性
- 宏被定义时的环境和被执行的环境是两个完全不同的概念
回头来看define-macro
的行为, 其实它的工作只是替换代码, 本身并不携带任何binding信息, 或者说, 它的binding信息来源于真正被执行的环境, 这个环境我们叫做动态作用域
那么, 显然在这种情况下, 我们需要一种能够拥有自己独立binding环境的宏. scheme在R5RS后提供了新的语法
define-syntax
从名字可以感觉出来, 这种宏应当是工作在语法层面, 形式大致如下
|
|
_
表示macro本身的placeholdersyntax-rules
则支持模式匹配
syntax-rules
模式匹配是一个很强大的功能, 可以很方便的定义一些含有递归结构的逻辑(scala中的pattern matching甚至能够一定程度上实现语义的匹配, 用起来也是非常顺手)
一个简单的例子:
|
|
可以看到let*
的定义非常简单, 甚至可以像普通方法一样进行递归定义
hygienic
读者可以尝试一下在let*
中进行各种”名字污染”行为(比如重新定义let
), 结果当然是可以正确执行. 因为define-syntax
引入了hygiene macro(卫生宏)的概念, 即: 宏内部使用的binding信息来源于被定义时的环境, 而不受到运行环境的影响, 这也叫作referential transparency. 对应的, 我们称hygiene macro工作在词法作用域
这是不是意味有了卫生宏的特性我们不需要动态作用域的功能了呢? 也不尽然, 看一个场景
|
|
show-vars
展示了当前环境下定义的变量的名字和内容, 而这是一个运行时的环境, 恰好define-macro
能够做到, 这也是词法作用域和动态作用域的区别
syntax-case
scheme也提供了比syntax-rules
更细粒度的语法控制能力(为什么这么说?), 其形式如下:
|
|
关于fender
的概念(不过这里没有用到):
If the optional
is present, it serves as an additional constraint on acceptance of a clause. If the of a given matches the input value, the corresponding is evaluated. If evaluates to a true value, the clause is accepted; otherwise, the clause is rejected as if the pattern had failed to match the value. Fenders are logically a part of the matching process, i.e., they specify additional matching constraints beyond the basic structure of the input.
对比一下两者的特点:
|
|
- 都支持pattern matching
syntax-case
的返回有#'
前缀: 实际上被用来替换在pattern matching里被捕获的模式变量- (语法上看不出来的)
syntax-case
提供了拆解和重组语法对象的能力, 即操作syntax-object的能力(什么是syntax-object? 我们先往下看)
datum & syntax object
比如我们想实现这样一个宏aif
:
(aif (getuid) (display it) (display "none"))
, it
是一个动态的binding, 显然aif需要工作在动态作用域
版本1
|
|
then
else
都是syntax-object, 在syntax-form中作为模板变量被替换时仅仅保留了各自的词法上下文(lexical scope), 因此它们都不能访问it
(因为在他们定义的环境中并没有it
, 这也是referential transparency的体现)
版本2
|
|
datum->syntax
用于把一个symbol变成给定syntax-form中的syntax-object#'
内部只会替换在pattern match里被捕获的模式变量, 其他的名字则引用自定义该macro时的词法上下文
虽然通过datum->syntax
引入了一层lexical scope, 但是请注意该scope是相对于x
(即整个(aif ..)
调用的syntax-object)来说的, 换句话说是aif
调用的上下文(例如(let ([..]) (aif ..))
, 则aif的上下文即let以及let的外层环境); 而在#'(let ((it test)) ..)'
中的it
仅仅是展开后的一个名字, 与datum->syntax
引入的it
并不是同一个东西, 尽管后者确实能被then/else引用到(如果有被定义的话, 而在这个例子里, 尽管它拥有可以被then/else访问的lexical scope, 但实际上环境里并没有定义它, 因而会出现unbound variable错误)
(hint: 在drracket里可以很方便的看到referencing的情况)
版本3
|
|
引入一层定义it
的环境, 然后再通过syntax-case
来捕捉到这个模式变量, 此时then/else所处的环境是在it
被引入且定义的环境中:
为了方便说明, 使用yy替换
|
|
所以实际上, 我们是在then/else调用时能访问到的环境中引入了it这个名字(并通过let定义); 假设我们不是通过syntax-case
来捕获到yy, 那么在模板中yy仅仅是一个名字(就像我们在repl里直接输入yy), 显然在then/else的lexical socpe里当然引用不到yy.
由此可以知道syntax-object是一个存在于某个上下文环境(有意义)的名字!
从aif
的例子也可以看到, define-syntax
也可以具有动态作用域的能力, 实际上我们想一下scheme里面为什么把syntax-rules/case
叫做transformer? 因为它们能够:
- 通过pattern matching来捕捉syntax-object(是不是像在分析语法树?)
- 捕捉到的syntax-object可以被直接eval, 也可以被再次拆解/修改/引入新的syntax-object, 并且它们都属于当前操作的syntax-form的binding(是不是感觉像在编辑语法树?)
- 没有被捕捉到的对象则保持定义时的binding, 从而避免了污染问题