Scheme元编程

同相性,指的是,

程序和程序所操作的数据采用了统一编码。


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宏的定义和使用做了简单介绍,

希望能揭开它的神秘面纱。