Racket编程指南——16 宏(macro)

16 宏(macro)

宏(macro)是一种语法表,它有一个关联的转换器(transformer),它将原有的表扩展(expand)为现有的表。换句话说,宏是Racket编译器的扩展。racket/baseracket的大部分句法表实际上是宏,扩展成一小部分核心结构。

像许多语言一样,Racket提供基于模式的宏,使得简单的转换易于实现和可靠使用。Racket还支持任意的宏转换器,它在Racket中实现,或在Racket中的宏扩展变体中实现。

(对于自下而上的Racket宏的介绍,你可以参考:《宏的担忧》)

    16.1 基于模式的宏

      16.1.1 define-syntax-rule

      16.1.2 词法范围

      16.1.3 define-syntaxsyntax-rules

      16.1.4 匹配序列

      16.1.5 标识符宏

      16.1.6 set!转化器

      16.1.7 宏生成宏

      16.1.8 扩展的例子:函数的引用调用(Call-by-Reference)

    16.2 通用宏转化器

      16.2.1 语法对象

      16.2.2 宏转化器程序

      16.2.3 混合模式和表达式:syntax-case

      16.2.4 with-syntaxgenerate-temporaries

      16.2.5 编译和运行时阶段

      16.2.6 通用阶段等级

        16.2.6.1 阶段和绑定

        16.2.6.2 阶段和模块

      16.2.7 语法污染


16.1 基于模式的宏

基于模式的宏(pattern-based macro)将任何与模式匹配的代码替换为使用与模式部分匹配的原始语法的一部分的扩展。

16.1.1 define-syntax-rule

创建宏的最简单方法是使用define-syntax-rule

(define-syntax-rule pattern template)

作为一个运行的例子,考虑交换宏swap,它将交换值存储在两个变量中。可以使用define-syntax-rule实现如下:

(define-syntax-rule (swap x y)
  (let ([tmp x])
    (set! x y)
    (set! y tmp)))

define-syntax-rule表绑定一个与单个模式匹配的宏。模式必须总是以一个开放的括号开头,后面跟着一个标识符,这个标识符在这个例子中是swap。在初始标识符之后,其它标识符是宏模式变量(macro pattern variable),可以匹配宏使用中的任何内容。因此,这个宏匹配这个表(swap form1 form2)给任何form1form2

define-syntax-rule中的模式之后是摸板(template)。模板用于替代与模式匹配的表,但模板中的模式变量的每个实例都替换为宏使用模式变量匹配的部分。例如,在

(swap first last)

模式变量x匹配firsty匹配last,于是扩展是

(let ([tmp first])
  (set! first last)
  (set! last tmp))
16.1.2 词法范围

假设我们使用swap宏来交换名为tmpother的变量:

(let ([tmp 5]
      [other 6])
  (swap tmp other)
  (list tmp other))

上述表达式的结果应为(6 5)。然而,这种swap的使用的单纯扩展是

(let ([tmp 5]
      [other 6])
  (swap tmp other)
  (list tmp other))

其结果是(5 6)。问题在于,这个单纯的扩展混淆了上下文中的tmp,那里swap与宏摸板中的tmp被使用。

Racket不会为了swap的上述使用生成单纯的扩展。相反,它会生成以下内容

(let ([tmp 5]
      [other 6])
  (let ([tmp_1 tmp])
    (set! tmp other)
    (set! other tmp_1))
  (list tmp other))

正确的结果在(6 5)。同样,在示例中

(let ([set! 5]
      [other 6])
  (swap set! other)
  (list set! other))

其扩展是

(let ([set!_1 5]
      [other 6])
  (let ([tmp_1 set!_1])
    (set! set!_1 other)
    (set! other tmp_1))
  (list set!_1 other))

因此局部set!绑定不会干扰宏模板引入的赋值。

换句话说,Racket的基于模式的宏自动维护词法范围,所以宏的实现者可以思考宏中的变量引用以及在同样的途径中作为函数和函数调用的宏使用。

16.1.3 define-syntaxsyntax-rules

define-syntax-rule表绑定一个与单一模式匹配的宏,但Racket的宏系统支持从同一标识符开始匹配多个模式的转换器。要编写这样的宏,程序员必须使用更通用的define-syntax表以及syntax-rules转换器表:

(define-syntax id
  (syntax-rules (literal-id ...)
    [pattern template]
    ...))

例如,假设我们希望一个rotate宏将swap概括为两个或三个标识符,因此

(let ([red 1] [green 2] [blue 3])
  (rotate red green)      ; swaps
  (rotate red green blue) ; rotates left
  (list red green blue))

生成(1 3 2)。我们可以使用syntax-rules实现 rotate

(define-syntax rotate
  (syntax-rules ()
    [(rotate a b) (swap a b)]
    [(rotate a b c) (begin
                     (swap a b)
                     (swap b c))]))

表达式(rotate red green)syntax-rules表中的第一个模式相匹配,因此扩展到(swapred green)。表达式(rotate red green blue)与第二个模式匹配,所以它扩展到(begin(swap red green) (swap green blue))

16.1.4 匹配序列

一个更好的rotate宏将允许任意数量的标识符,而不是只有两个或三个标识符。匹配任何数量的标识符的rotate使用,我们需要一个模式表,它有点像克林闭包(Kleene star)。在一个Racket宏模式中,一个闭包(star)被写成...

为了用...实现rotate,我们需要一个基元(base case)来处理单个标识符,以及一个归纳案例以处理多个标识符:

(define-syntax rotate
  (syntax-rules ()
    [(rotate a) (void)]
    [(rotate a b c ...) (begin
                          (swap a b)
                          (rotate b c ...))]))

当在一种模式中像c这样的模式变量被...跟着的时候,它在模板中必须也被...跟着。模式变量有效地匹配一个零序列或多个表,并在模板中以相同的顺序被替换。

到目前为止,rotate的两种版本都有点效率低下,因为成对交换总是将第一个变量的值移动到序列中的每个变量,直到达到最后一个变量为止。更有效的rotate将第一个值直接移动到最后一个变量。我们可以用...模式使用辅助宏去实现更有效的变体:

(define-syntax rotate
  (syntax-rules ()
    [(rotate a c ...)
     (shift-to (c ... a) (a c ...))]))
 
(define-syntax shift-to
  (syntax-rules ()
    [(shift-to (from0 from ...) (to0 to ...))
     (let ([tmp from0])
       (set! to from) ...
       (set! to0 tmp))]))

shift-to宏里,在模板里的...后面跟着(set! to from),它导致(set! to from)表达式在tofrom序列中与必须使用的每个标识符匹配被复制一样多次。(tofrom匹配的数量必须相同,否则宏扩展就会有一个错误的失败。)

16.1.5 标识符宏

根据我们的宏定义,swaprotate标识符必须在开括号之后使用,否则会报告语法错误:

> (+ swap 3)

eval:2:0: swap: bad syntax

  in: swap

标识符宏(identifier macro)是一个模式匹配宏,当它被自己使用时不使用括号。例如,我们可以定义val为一个标识符宏,扩展到(get-val),所以(+ val 3)将扩展到(+ (get-val) 3)

> (define-syntax val
    (lambda (stx)
      (syntax-case stx ()
        [val (identifier? (syntax val)) (syntax (get-val))])))
> (define-values (get-val put-val!)
    (let ([private-val 0])
      (values (lambda () private-val)
              (lambda (v) (set! private-val v)))))
> val

0

> (+ val 3)

3

val宏使用syntax-case,它可以定义更强大的宏,并在《混合模式和表达式:syntax-case》中讲解。现在,知道定义宏是必要的,在lambda中使用了syntax-case,它的模板必须用显式syntax构造器包装。最后,syntax-case子句可以指定模式后面的附加保护条件。

我们的val宏使用identifier?条件确保在括号中val不能(must not)使用。相反,宏引一个发语法错误:

> (val)

eval:8:0: val: bad syntax

  in: (val)

16.1.6 set!转化器

使用上面的val宏,我们仍然必须调用put-val!更改存储值。然而,直接在val上使用set!会更方便。当val用于set!时借助宏,我们用make-set!-transformer创建一个赋值转换器(assignment transformer)。我们还必须声明set!作为syntax-case文本列表中的文字。

> (define-syntax val2
    (make-set!-transformer
     (lambda (stx)
       (syntax-case stx (set!)
         [val2 (identifier? (syntax val2)) (syntax (get-val))]
         [(set! val2 e) (syntax (put-val! e))]))))
> val2

0

> (+ val2 3)

3

> (set! val2 10)
> val2

10

16.1.7 宏生成宏

假设我们有许多标识符像valval2,我们想重定向给访问器和突变函数像get-valput-val!。我们希望可以只写:

(define-get/put-id val get-val put-val!)

自然地,我们可以实现define-get/put-id为一个宏:

> (define-syntax-rule (define-get/put-id id get put!)
    (define-syntax id
      (make-set!-transformer
       (lambda (stx)
         (syntax-case stx (set!)
           [id (identifier? (syntax id)) (syntax (get))]
           [(set! id e) (syntax (put! e))])))))
> (define-get/put-id val3 get-val put-val!)
> (set! val3 11)
> val3

11

define-get/put-id宏就是是一个宏生成宏(macro-generating macro)

16.1.8 扩展的例子:函数的引用调用(Call-by-Reference)

我们可以使用模式匹配宏将一个表添加到Racket中,以定义引用调用函数(call-by-reference function)的一阶调用。当通过参考函数本体转变它的正式参数,这个转变应用到变量,它在对函数的调用中作为一个实参提供。

例如,如果define-cbr类似于define,除了定义应用调用函数,那么

(define-cbr (f a b)
  (swap a b))
 
(let ([x 1] [y 2])
  (f x y)
  (list x y))

生成(2 1)

我们会通过有函数调用支持的对参数的访问器和转换器执行参考函数,而不是直接提供参数值。特别是,对于上面的函数f,我们将生成

(define (do-f get-a get-b put-a! put-b!)
  (define-get/put-id a get-a put-a!)
  (define-get/put-id b get-b put-b!)
  (swap a b))

并将函数调用(f x y)重定向到

(do-f (lambda () x)
      (lambda () y)
      (lambda (v) (set! x v))
      (lambda (v) (set! y v)))

显然,然后define-cbr是宏生成宏,绑定f到一个宏,它扩展到do-f的一个调用。即(define-cbr (f a b) (swap a b))需要生成的定义

(define-syntax f
  (syntax-rules ()
    [(id actual ...)
     (do-f (lambda () actual)
           ...
           (lambda (v)
             (set! actual v))
           ...)]))

同时,define-cbr需要使用f本体去定义do-f,第二部分是略微更复杂些,所以我们延迟它的大部分给一个define-for-cbr辅助模块,它可以让我们足够简单地编写define-cbr

(define-syntax-rule (define-cbr (id arg ...) body)
  (begin
    (define-syntax id
      (syntax-rules ()
        [(id actual (... ...))
         (do-f (lambda () actual)
               (... ...)
               (lambda (v)
                 (set! actual v))
               (... ...))]))
    (define-for-cbr do-f (arg ...)
      () ; explained below...
      body)))

我们剩下的任务是定义define-for-cbr以便它转换

(define-for-cbr do-f (a b) () (swap a b))

到上边的这个函数定义do-f两个功能定义。大部分的工作是生成一个define-get/put-id声明给每个参数,ab,以及把他们放在本体之前。通常,对于在模式和模板中的...那是很容易的任务,但这次这里有一个捕获:我们需要既生成这个名字get-aput-a!也要生成get-bput-b!,这个模式语言没有办法提供基于现有标识符的综合标识符。

事实证明,词法范围给了我们解决这个问题的方法。诀窍是为函数中的每个参数迭代一次对define-for-cbr的扩展,这就是为什么define-for-cbr开始用一个在参数列表后面明显无效的()的原因。除了要处理的参数外,我们还需要跟踪迄今为止所看到的所有参数以及为每个生成的getput名称。在处理完所有的标识符之后,我们就拥有了所有需要的名称。

这里是define-for-cbr的定义:

(define-syntax define-for-cbr
  (syntax-rules ()
    [(define-for-cbr do-f (id0 id ...)
       (gens ...) body)
     (define-for-cbr do-f (id ...)
       (gens ... (id0 get put)) body)]
    [(define-for-cbr do-f ()
       ((id get put) ...) body)
     (define (do-f get ... put ...)
       (define-get/put-id id get put) ...
       body)]))

一步一步,展开如下:

(define-for-cbr do-f (a b)
  () (swap a b))
=> (define-for-cbr do-f (b)
     ([a get_1 put_1]) (swap a b))
=> (define-for-cbr do-f ()
     ([a get_1 put_1] [b get_2 put_2]) (swap a b))
=> (define (do-f get_1 get_2 put_1 put_2)
     (define-get/put-id a get_1 put_1)
     (define-get/put-id b get_2 put_2)
     (swap a b))

get_1get_2put_1put_2上的“下标(subscript)”通过宏扩展插入到保留词法范围,因为getdefine-for-cbr的每一次迭代生成不应捆绑被不同的迭代生成的get。换句话说,我们本质上欺骗这个宏扩展以生成的我们新的名字,但技术显示了一些与自动词法范围的宏模式的神奇力量。

最后表达式最终扩展成

(define (do-f get_1 get_2 put_1 put_2)
  (let ([tmp (get_1)])
    (put_1 (get_2))
    (put_2 tmp)))

它实现了名称调用(call-by-name)函数f

接下来,总结一下,我们可以只用三个基于模式的宏添加引用调用(call-by-reference)函数到Racket中:define-cbrdefine-for-cbrdefine-get/put-id

16.2 通用宏转化器

define-syntax表为标识符创建一个转换器绑定(transformer binding),这是一个可以在编译时使用的绑定,同时扩展表达式以在运行时进行求值。与转换器绑定相关联的编译时间值可以是任何东西;如果它是一个参数的过程,则绑定用作宏,而过程是 宏转换器(macro transformer)

    16.2.1 语法对象

    16.2.2 宏转化器程序

    16.2.3 混合模式和表达式:syntax-case

    16.2.4 with-syntaxgenerate-temporaries

    16.2.5 编译和运行时阶段

    16.2.6 通用阶段等级

      16.2.6.1 阶段和绑定

      16.2.6.2 阶段和模块

    16.2.7 语法污染


16.2.1 语法对象

宏转换器(即源和替换表)的输入和输出被表示为语法对象(syntax object)。语法对象包含符号、列表和常量值(如数字),它们基本上与表达式的quote(引用)表相对应。例如,表达式描述为(+ 1 2)包含符号'+和数字12,都在列表中。除了引用的内容之外,语法对象还将源位置和词汇绑定信息与表的每个部分关联起来。在报告语法错误时使用源位置信息(例如),词汇绑定信息允许宏系统维护词法范围。为了适应这种额外的信息,表达式描述为(+ 1 2)不仅是'(+ 1 2),但'(+ 1 2)的封装成为了语法对象。

要创建文字语法对象,请使用syntax表:

> (syntax (+ 1 2))

#

在同样的方式,'省略了quote#'省略了syntax

> #'(+ 1 2)

#

只包含符号的语法对象是标识符语法对象(identifier syntax object)。它提供了一些特定于标识符语法对象的附加操作,包括identifier?操作以检查操作符。最值得注意的是,free-identifier=?确定两个标识符是否引用相同的绑定:

> (identifier? #'car)

#t

> (identifier? #'(+ 1 2))

#f

> (free-identifier=? #'car #'cdr)

#f

> (free-identifier=? #'car #'car)

#t

> (require (only-in racket/base [car also-car]))
> (free-identifier=? #'car #'also-car)

#t

要在语法对象中看到列表、符号、数字、etc.等等,请使用syntax->datum

> (syntax->datum #'(+ 1 2))

'(+ 1 2)

syntax-e函数类似于syntax->datum,但它打开了一个单层的源位置和词汇上下文信息,离开有它们自己的信息作为语法的对象子表:

> (syntax-e #'(+ 1 2))

'(# # #)

syntax-e函数总是放弃语法对象包装器,它包围着子表,子表表示为通过符号、数值,和其它文本值。唯一的一次额外的子表打开时展开一个配对,在这种情况下,配对的cdr可以根据语法构建的对象递归地展开。

当然,与syntax->datum相对立的是datum->syntax。除了像'(+ 1 2)这样的数据外,datum->syntax还需要一个现有的语法对象来贡献它的词法上下文,并且可以选择另一个语法对象来贡献它的源位置:

> (datum->syntax #'lex
                 '(+ 1 2)
                 #'srcloc)

#

在上面的例子中,对#'lex的词法上下文是用于新的语法对象,而#'srcloc源位置被使用。

datum->syntax的第二个(即,“datum”)参数包含语法对象时,这些语法对象将原封不动地保存在结果中。那就是,用syntax-e解构的结果最终产生了给予datum->syntax的这个语法对象。

16.2.2 宏转化器程序

任何一个参数的过程都可以是一个宏转换器(macro transformer)。事实证明,syntax-rules(语法规则)表是一个扩展为过程表的宏。例如,如果直接求值syntax-rules表(而不是放在define-syntax表的右侧),结果就是一个过程:

> (syntax-rules () [(nothing) something])

#

可以使用lambda直接编写自己的宏转换器过程,而不是使用syntax-rules。对过程的参数是表示源表的语法对象(syntax object),过程的结果必须是表示替换表的语法对象(syntax object)

> (define-syntax self-as-string
    (lambda (stx)
      (datum->syntax stx
                     (format "~s" (syntax->datum stx)))))
> (self-as-string (+ 1 2))

"(self-as-string (+ 1 2))"

传递给宏转换器的源表表示一个表达式,其中在应用程序位置(即在启动表达式的括号之后)使用其标识符,或者如果它被用作表达式位置而不是应用程序位置,则它本身代表标识符。

> (self-as-string (+ 1 2))

"(self-as-string (+ 1 2))"

> self-as-string

"self-as-string"

define-syntax表支持与define的函数一样的快捷语法,因此下面的self-as-string定义等同于显式使用lambda的那个定义:

> (define-syntax (self-as-string stx)
    (datum->syntax stx
                   (format "~s" (syntax->datum stx))))
> (self-as-string (+ 1 2))

"(self-as-string (+ 1 2))"

16.2.3 混合模式和表达式:syntax-case

通过syntax-rules生成的程序在内部使用syntax-e解构了语法对象,并使用datum->syntax以构造结果。syntax-rules表没有提供一种方法从模式匹配和模板构建模式中跳转到任意的Racket表达式中。

syntax-case表允许混合模式匹配、模板构造和任意表达式:

(syntax-case stx-expr (literal-id ...)
  [pattern expr]
  ...)

syntax-rules不同,syntax-case表不产生过程。相反,它从一个stx-expr表达式决定的语法对象来匹配pattern。另外,每个syntax-case有一个pattern和一个expr,而不是一个patterntemplate。在一个expr里,syntax表——通常用#'缩写——进入模板构造方式;如果一个从句的expr#'开始,那么我们就会获得一些像syntax-rules的表:

> (syntax->datum
   (syntax-case #'(+ 1 2) ()
    [(op n1 n2) #'(- n1 n2)]))

'(- 1 2)

我们可以使用syntax-case来编写swap宏,以代替define-syntax-rulesyntax-rules

(define-syntax (swap stx)
  (syntax-case stx ()
    [(swap x y) #'(let ([tmp x])
                    (set! x y)
                    (set! y tmp))]))

使用syntax-case的一个优点是,我们可以为swap提供更好的错误报告。例如,使用swapdefine-syntax-rule定义,然后(swap x 2)set!条件中产生语法错误!,因为2不是一个标识符。我们可以改进swapsyntax-case实现来显式检查子表:

(define-syntax (swap stx)
  (syntax-case stx ()
    [(swap x y)
     (if (and (identifier? #'x)
              (identifier? #'y))
         #'(let ([tmp x])
             (set! x y)
             (set! y tmp))
         (raise-syntax-error #f
                             "not an identifier"
                             stx
                             (if (identifier? #'x)
                                 #'y
                                 #'x)))]))

通过这个定义,(swap x 2)提供了一个源自swap而不是set!的语法错误。

在互换上述swap的定义里,#'x#'y是模板,即使它们不是作为宏转换器的结果。这个例子说明了如何使用模板来访问输入语法的片段,在这种情况下可以检查碎片的表。同时,在对raise-syntax-error的调用中对#'x#'y的匹配被使用,所以语法错误信息可以直接指到非标识符的源位置。

16.2.4 with-syntaxgenerate-temporaries

由于syntax-case允许我们用任意的Racket表达式进行计算,我们可以更简单地解决我们在编写define-for-cbr(参见《扩展的例子:函数的引用调用(Call-by-Reference)》)中的一个问题,在这里我们需要根据序列id ...生成一组名称:

(define-syntax (define-for-cbr stx)
  (syntax-case stx ()
    [(_ do-f (id ...) body)
     ....
       #'(define (do-f get ... put ...)
           (define-get/put-id id get put) ...
           body) ....]))

代替上面的....我们需要绑定get ...put ...到生成标识符的列表。我们不能使用let绑定getput,因为我们需要绑定那个计数作为模式变量,而不是普通的局部变量。with-syntax表允许我们绑定模式变量:

(define-syntax (define-for-cbr stx)
  (syntax-case stx ()
    [(_ do-f (id ...) body)
     (with-syntax ([(get ...) ....]
                   [(put ...) ....])
       #'(define (do-f get ... put ...)
           (define-get/put-id id get put) ...
           body))]))

现在我们需要一个表达代替....它生成与在原始模式中匹配id一样多的标识符。由于这是一个常见的任务,Racket提供了一个辅助函数,generate-temporaries,以一系列的标识符并返回一个序列生成的标识符:

(define-syntax (define-for-cbr stx)
  (syntax-case stx ()
    [(_ do-f (id ...) body)
     (with-syntax ([(get ...) (generate-temporaries #'(id ...))]
                   [(put ...) (generate-temporaries #'(id ...))])
       #'(define (do-f get ... put ...)
           (define-get/put-id id get put) ...
           body))]))

这种方式产生的标识符通常比欺骗宏扩展产生的纯粹基于模式的宏的名字更容易理解。

一般来说,一个with-syntax绑定左边是一个模式,就像在syntax-case中一样。事实上,一个with-syntax表只是一个syntax-case的部分转换。

16.2.5 编译和运行时阶段

随着宏集合越来越多复杂,你可能要写你自己的辅助函数,如generate-temporaries。例如,提供良好的语法错误信息, swaprotatedefine-cbr都应该检查在源表中的某一个子表是标识符。我们可以使用check-ids函数在任何地方执行此检查:

(define-syntax (swap stx)
  (syntax-case stx ()
    [(swap x y) (begin
                  (check-ids stx #'(x y))
                  #'(let ([tmp x])
                      (set! x y)
                      (set! y tmp)))]))
 
(define-syntax (rotate stx)
  (syntax-case stx ()
    [(rotate a c ...)
     (begin
       (check-ids stx #'(a c ...))
       #'(shift-to (c ... a) (a c ...)))]))

check-ids函数可以使用syntax->list函数将一个语法对象转换成一个语法对象列表:

(define (check-ids stx forms)
  (for-each
   (lambda (form)
     (unless (identifier? form)
       (raise-syntax-error #f
                           "not an identifier"
                           stx
                           form)))
   (syntax->list forms)))

然而,如果以这种方式定义swapcheck-ids,则它不会运行:

> (let ([a 1] [b 2]) (swap a b))

check-ids: undefined;

 cannot reference undefined identifier

问题是check-ids被定义为一个运行时表达式,但是swap试图在编译时使用它。在交互模式中,编译时和运行时是交错的,但它们不是在模块的主体内交错的,它们不在预编译的模块之间进行交叉。为了帮助所有这些模式一致地对待代码,Racket将不同阶段的绑定空间分隔开来。

要定义可在编译时引用的check-ids函数,使用begin-for-syntax

(begin-for-syntax
  (define (check-ids stx forms)
    (for-each
     (lambda (form)
       (unless (identifier? form)
         (raise-syntax-error #f
                             "not an identifier"
                             stx
                             form)))
     (syntax->list forms))))

使用此语法定义,那么swap就会运行:

> (let ([a 1] [b 2]) (swap a b) (list a b))

'(2 1)

> (swap a 1)

eval:13:0: swap: not an identifier

  at: 1

  in: (swap a 1)

当将程序组织成模块时,你也许希望将辅助函数放在一个模块中,以供驻留在其它模块上的宏使用。在这种情况下,你可以使用define编写辅助函数:

"utils.rkt"

#lang racket
 
(provide check-ids)
 
(define (check-ids stx forms)
  (for-each
   (lambda (form)
     (unless (identifier? form)
       (raise-syntax-error #f
                           "not an identifier"
                           stx
                           form)))
   (syntax->list forms)))

然后,在实现宏模块中,使用(require (for-syntax "utils.rkt"))代替(require"utils.rkt")导入辅助函数:

#lang racket
 
(require (for-syntax "utils.rkt"))
 
(define-syntax (swap stx)
  (syntax-case stx ()
    [(swap x y) (begin
                  (check-ids stx #'(x y))
                  #'(let ([tmp x])
                      (set! x y)
                      (set! y tmp)))]))

因为模块分别编译并没有循环依赖,"utils.rkt"模块的运行时本体可以在编译的模块实现swap编译。因此,在"utils.rkt"可以用来实现swap,只要他们通过(require (for-syntax ....))明确转移到编译时间。

racket模块提供了syntax-casegenerate-temporarieslambdaif以及更多以用在运行时阶段和编译时阶段。这就是为什么我们既可以直接在racketREPL中也可以在一个define-syntax表的右端使用syntax-case的原因。

与此相反,racket/base模块只在运行时阶段导出这些绑定。如果你改变上面定义swap的模块,使它使用racket/base语言,而不是racket,那么它不再运行。添加(require(for-syntax racket/base))导入syntax-case和更多进入编译时阶段,使模块再次工作。

假设define-syntax用于在define-syntax表的右端定义一个局部宏。在这种情况下,内部define-syntax的右端位于元编译阶段等级(meta-compile phase level),也称为阶段等级2(phase level 2)。要将syntax-case导入到该阶段等级,你必须使用(require(for-syntax (for-syntax racket/base))),或者等效地使用(require (for-meta 2racket/base))。例如,

#lang racket/base
(require  ;; This provides the bindings for the definition
          ;; of shell-game.
          (for-syntax racket/base)
 
          ;; And this for the definition of
          ;; swap.
          (for-syntax (for-syntax racket/base)))
 
(define-syntax (shell-game stx)
 
  (define-syntax (swap stx)
    (syntax-case stx ()
      [(_ a b)
       #'(let ([tmp a])
           (set! a b)
           (set! b tmp))]))
 
  (syntax-case stx ()
    [(_ a b c)
     (let ([a #'a] [b #'b] [c #'c])
       (when (= 0 (random 2)) (swap a b))
       (when (= 0 (random 2)) (swap b c))
       (when (= 0 (random 2)) (swap a c))
       #`(list #,a #,b #,c))]))
 
(shell-game 3 4 5)
(shell-game 3 4 5)
(shell-game 3 4 5)

反向阶段等级也存在。如果一个宏使用了一个导入for-syntax的辅助函数,如果辅助函数返回由syntax生成的语法对象常量,那么语法中的标识符将需要在阶段等级-1(phase level -1),也称为模板阶段等级(template phase level),以便在运行时阶段等级相对于定义宏的模块有任何绑定。

例如,在下面的例子中没有语法变换器的swap-stx的辅助函数——它只是一个普通的函数——但它产生的语法对象得到拼接成shell-game的结果。因此,其包含的辅助模块需要在shell-game阶段1用(require (for-syntax 'helper))导入。

但从swap-stx的角度,当被shell-game返回的语法求值时,其结果最终在阶段1求值。换句话说,一个负向阶段等级是一个从正方向来看的相反的方阶段等级:shell-game的阶段1是swap-stx的阶段0,所以shell-game的阶段0是swap-stx的阶段-1。这就是为什么这个例子不运行——'helper子模块在阶段-1没有绑定。

#lang racket/base
(require (for-syntax racket/base))
 
(module helper racket/base
  (provide swap-stx)
  (define (swap-stx a-stx b-stx)
    #`(let ([tmp #,a-stx])
          (set! #,a-stx #,b-stx)
          (set! #,b-stx tmp))))
 
(require (for-syntax 'helper))
 
(define-syntax (shell-game stx)
  (syntax-case stx ()
    [(_ a b c)
     #`(begin
         #,(swap-stx #'a #'b)
         #,(swap-stx #'b #'c)
         #,(swap-stx #'a #'c)
         (list a b c))]))
 
(define x 3)
(define y 4)
(define z 5)
(shell-game x y z)

修复这个例子,我们添加(require (for-template racket/base))'helper子模块。

#lang racket/base
(require (for-syntax racket/base))
 
(module helper racket/base
  (require (for-template racket/base)) ; binds `let` and `set!` at phase -1
  (provide swap-stx)
  (define (swap-stx a-stx b-stx)
    #`(let ([tmp #,a-stx])
          (set! #,a-stx #,b-stx)
          (set! #,b-stx tmp))))
 
(require (for-syntax 'helper))
 
(define-syntax (shell-game stx)
  (syntax-case stx ()
    [(_ a b c)
     #`(

你可能感兴趣的:(Lisp,Racket编程指南(中文译),Racket)