这只是我阅读Fear of Macros记下的笔记.
简而言之, 一个句法转换器接受句法而返回句法. 换言之, 其对于句法进行变换.
以下是一个转换器的例子, 其直接忽略了输入的句法.
> (define-syntax foo
(lambda (stx)
(syntax "I am foo")))
> (foo)
"I am foo"
使用define-syntax
创建了转换器的绑定.恰如define
, define-syntax
也有类似的句法糖.
> (define-syntax (also-foo stx)
(syntax "I am also foo"))
> (also-foo)
"I am also foo"
恰如'
是quote
的简记法, #'
是syntax
的简记法.
> (define-syntax (quoted-foo stx)
#'"I am also foo, using #' instead of syntax")
> (quoted-foo)
"I am also foo, using #' instead of syntax"
当然, 返回的句法不仅限于字符串字面量.
> (define-syntax (say-hi stx)
#'(displayln "hi"))
> (say-hi)
hi
之前的例子只是直接忽略了输入的句法, 但一般情况下我们总是想要输入的句法转换为别的什么东西. 首先, 让我们来仔细观察一下输入的究竟是什么.
> (define-syntax (show-me stx)
(print stx)
#'(void))
> (show-me '(+ 1 2))
#<syntax:eval:10:0 (show-me (quote (+ 1 2)))>
从中可以看出, 转换器接受的是一个句法对象 (syntax object).一个句法对象除了字面, 还包含了诸多有趣的信息, 例如其位置还有关于词法作用域的东西. {译注: 因此, 读者会发现这里的(交互所呈现的)句法对象的信息, 大概和自己试验时不太一样.}
存在着各种各样可以访问句法对象的函数. 首先, 让我们定义一个句法.
> (define stx #'(if x (list "true") #f))
> stx
#<syntax:eval:11:0 (if x (list "true") #f)>
然后, 以下是一些用于获取源信息的函数.> (syntax-source stx)
'eval
> (syntax-line stx)
11
> (syntax-column stx)
0
更有趣的是句法字面本身, 我们可以用syntax->datum
将其转换为一个S-expression.> (syntax->datum stx)
'(if x (list "true") #f)
与之相对的是, syntax-e
只往下走一层.> (syntax-e stx)
'(#<syntax:eval:11:0 if> #<syntax:eval:11:0 x> #<syntax:eval:11:0 (list "true")> #<syntax:eval:11:0 #f>)
还有一个函数叫做syntax->list
, 某些时候和syntax-e
表现类似, 但其实相当不同.> (syntax->list stx)
'(#<syntax:eval:11:0 if> #<syntax:eval:11:0 x> #<syntax:eval:11:0 (list "true")> #<syntax:eval:11:0 #f>)
现在让我们写一个转换器函数, 其将输入的句法颠倒.
> (define-syntax (reverse-me stx)
(datum->syntax stx (reverse (cdr (syntax->datum stx)))))
> (reverse-me "backwards" "am" "i" values)
"i"
"am"
"backwards"
datum->syntax
的第一个参数包含了我们想要与输出的句法对象关联的词法上下文信息. 如果其被设置为#f
, 那么就是没有信息.通常的Racket代码运行在运行时, 这是显而易见的. 但是, 转换器被Racket调用是parse, expand, 编译程序这一过程的组成部分. 换言之, 句法转换器函数在编译时被求值. 当然, 也有人会说句法阶段
和运行时阶段
, 只不过是相同概念的不同说法而已.
先让我们回顾一下为什么要有宏的存在. 一个经典的例子可能是Racket的if
形式.
(if <condition> <true-expression> <false-expression>)
如果我们将if
实现为一个函数, 那么所有的参数都会在提供给函数之前被求值.> (define (our-if condition true-expr false-expr)
(cond [condition true-expr]
[else false-expr]))
> (our-if #t
"true"
"false")
"true"
似乎这能够成立, 然而请看以下交互.> (define (display-and-return x)
(displayln x)
x)
> (our-if #t
(display-and-return "true")
(display-and-return "false"))
true
false
"true"
这暗示我们if
并不可能是一个平然的函数. 然而, 句法转换器可以帮助我们完成, 因为其会在编译时对于句法进行重写, 但直到运行时并不会实际进行求值.> (define-syntax (our-if-v2 stx)
(define xs (syntax->list stx))
(datum->syntax stx `(cond [,(cadr xs) ,(caddr xs)]
[else ,(cadddr xs)])))
> (our-if-v2 #t
(display-and-return "true")
(display-and-return "false"))
true
"true"
> (our-if-v2 #f
(display-and-return "true")
(display-and-return "false"))
false
"false"
这给出了正确的答案, 但它是如何运作的呢? 让我们抽出转换器本身, 看看它到底做了什么. 首先, 让我们从一个输入句法作为例子开始.
> (define stx #'(our-if-v2 #t "true" "false"))
> (displayln stx)
#<syntax:eval:32:0 (our-if-v2 #t "true" "false")>
1. 我们取原始的句法, 使用syntax->list
以将其转换为一个句法对象的列表.> (define xs (syntax->list stx))
> (displayln xs)
(#<syntax:eval:32:0 our-if-v2> #<syntax:eval:32:0 #t> #<syntax:eval:32:0 "true"> #<syntax:eval:32:0 "false">)
2. 为了将其转换为cond
形式, 我们需要从列表中取出我们所感兴趣的三个部分, 通过使用cadr
, caddr
, cadddr
, 之后的安排则是顺理成章的.`(cond [,(cadr xs) ,(caddr xs)]
[else ,(cadddr xs)])
3. 最后, 我们使用datum->syntax
以将其转换回为一个句法对象.> (datum->syntax stx `(cond [,(cadr xs) ,(caddr xs)]
[else ,(cadddr xs)]))
#<syntax (cond (#t "true") (else "false"))>
我们大概已经明白了其工作的流程, 然而使用这些函数来解构列表并不是很清晰自然的事情, 而且也很容易出错. 因此, 我们想到可以使用Racket提供的模式匹配机制match
.我们想着与其写成
> (define-syntax (our-if-v2 stx)
(define xs (syntax->list stx))
(datum->syntax stx `(cond [,(cadr xs) ,(caddr xs)]
[else ,(cadddr xs)])))
不如写成> (define-syntax (our-if-using-match stx)
(match (syntax->list stx)
[(list name condition true-expr false-expr)
(datum->syntax stx `(cond [,condition ,true-expr]
[else ,false-expr]))]))
然后, 我们会遇到> (our-if-using-match #t "true" "false")
match: undefined;
cannot reference an identifier before its definition
in module: 'program
其在抱怨match
并没有被定义.我们的转换器函数在编译时工作, 然而编译时能用的只是racket/base
, 并非完整的Racket语言. 为了使用超出racket/base
的东西, 我们需要require
的for-syntax
形式.
> (require (for-syntax racket/match))
> (define-syntax (our-if-using-match-v2 stx)
(match (syntax->list stx)
[(list _ condition true-expr false-expr)
(datum->syntax stx `(cond [,condition ,true-expr]
[else ,false-expr]))]))
> (our-if-using-match-v2 #t "true" "false")
"true"
begin-for-syntax
之前我们已经用了for-syntax
来require
需要在编译时用到的racket/match
模块. 不过, 显然有时我们也需要自己定义宏所需的辅助函数. 通常的define
无法解决问题, 因为正如你所知的, 其出现于运行时而非编译时. 然而, 我们可以使用begin-for-syntax
.
(begin-for-syntax
(define (my-helper-function ....)
....))
(define-syntax (macro-using-my-helper-function stx)
(my-helper-function ....)
....)
其实也可以使用define-for-syntax
.(define-for-syntax (my-helper-function ....)
....)
(define-syntax (macro-using-my-helper-function stx)
(my-helper-function ....)
....)
syntax-case
和syntax-rules
绝大部分有用的句法转换器都是对于输入句法的组成部分的重新排列, 因此我们应该使用模式匹配. 实际上, Racket本来就有基于模式匹配的宏系统, 例如syntax-rules
和syntax-case
. {译注: Schemer都知道, 不只是Racket提供这两个, 其他许多Scheme实现也会提供这两个. 另外, 原文说Racket的define-syntax-rule
是基于syntax-case
的, 这种说法有点问题, 因为实际上它是基于syntax-rules
的.}
基于syntax-case
, 我们之前的例子
(require (for-syntax racket/match))
(define-syntax (our-if-using-match-v2 stx)
(match (syntax->list stx)
[(list _ condition true-expr false-expr)
(datum->syntax stx `(cond [,condition ,true-expr]
[else ,false-expr]))]))
可以写成> (define-syntax (our-if-using-syntax-case stx)
(syntax-case stx ()
[(_ condition true-expr false-expr)
#'(cond [condition true-expr]
[else false-expr])]))
> (our-if-using-syntax-case #t "true" "false")
"true"
这看起来其实和之前的也相当类似, 但是免去了使用datum->syntax
还有准引用. 当然, 这种情况下使用define-syntax-rule
也差不多, 甚至还更简单一点.> (define-syntax-rule (our-if-using-syntax-rule condition true-expr false-expr)
(cond [condition true-expr]
[else false-expr]))
> (our-if-using-syntax-rule #t "true" "false")
"true"
Racket的struct
可以完成一种有趣的事情, 即例如
(struct foo (field1 field2))
那么其会生成一些新的名字, 包括foo-field1
, foo-field2
, foo?
.让我们也用宏做点类似的事情. 比如说, 我们想要将句法(hyphen-define a b (args) body)
转换为(define (a-b args) body)
.
以下是一次错误的尝试.
> (define-syntax (hyphen-define/wrong1 stx)
(syntax-case stx ()
[(_ a b (args ...) body0 body ...)
(let ([name (string->symbol (format "~a-~a" a b))])
#'(define (name args ...)
body0 body ...))]))
eval:47:0: a: pattern variable cannot be used outside of a
template
in: a
当然, 现在读者还无法理解错误信息的含义. 不过, 我要说明所谓的模板 (template) 指的是诸如#'(define (name args ...) body0 body ...)
这样的部分. 那么, 看起来像是我们在这let
绑定里不能使用a
(或者b
).实际上, syntax-case
里你想用多少模板就可以有多少. 虽然我们不能直接使用模式变量, 但是我们可以将其置于#'
之中. 也就是说, 我们或许可以尝试一下如下定义.
> (define-syntax (hyphen-define/wrong1.1 stx)
(syntax-case stx ()
[(_ a b (args ...) body0 body ...)
(let ([name (string->symbol (format "~a-~a" #'a #'b))])
#'(define (name args ...)
body0 body ...))]))
> (hyphen-define/wrong1.1 foo bar () #t)
> (foo-bar)
foo-bar: undefined;
cannot reference an identifier before its definition
in module: 'program
如你所见, 虽然定义时没有出错, 但是仍然并不符合我们的预期.实际上, 如果你使用宏步进器, 就会发现
(hyphen-define/wrong1.1 foo bar () #t)
会被扩展为(define (name) #t)
而非我们预期的(define (foo-bar) #t)
我们的模板使用了符号name
, 但是我们期望其能够使用name
的值.回想一下, 有什么出现在模板里的变量是使用其值的呢? 显然, 模式变量是我们已知的答案. 或许我们可以想象一种变通的解决方案, 也就是使用两次syntax-case
, 其中一次用于创建模式变量name
.
> (define-syntax (hyphen-define/wrong1.2 stx)
(syntax-case stx ()
[(_ a b (args ...) body0 body ...)
(syntax-case (datum->syntax #'a
(string->symbol (format "~a-~a" #'a #'b)))
()
[name #'(define (name args ...)
body0 body ...)])]))
> (hyphen-define/wrong1.2 foo bar () #t)
> (foo-bar)
foo-bar: undefined;
cannot reference an identifier before its definition
in module: 'program
这个定义看起来有点奇怪, 虽然看起来也很合理. 但是, 仍然并不正确.或许我们应该继续使用宏步进器看看我们的问题究竟出在了哪里. 实际上, 我们的
(hyphen-define/wrong1.2 foo bar () #t)
被转换为了(define (|#<syntax:11:24foo>-#<syntax:11:28 bar>|) #t)
这下真相大白了, 问题在于通过#'a
和#'b
, 我们得到的其实是句法对象. 因此, 为了达成我们的预期, 我们应该使用syntax->datum
进行转换.> (define-syntax (hyphen-define/ok1 stx)
(syntax-case stx ()
[(_ a b (args ...) body0 body ...)
(syntax-case (datum->syntax #'a
(string->symbol (format "~a-~a"
(syntax->datum #'a)
(syntax->datum #'b))))
()
[name #'(define (name args ...)
body0 body ...)])]))
> (hyphen-define/ok1 foo bar () #t)
> (foo-bar)
#t
终于结束了! 接下来我们要引入一些帮助我们编写宏的简便方法.with-syntax
与其使用嵌套的syntax-case
, 其实我们可以使用with-syntax
. 从某种意义上说, with-syntax
长得有点像let
.
> (define-syntax (hyphen-define/ok2 stx)
(syntax-case stx ()
[(_ a b (args ...) body0 body ...)
(with-syntax ([name (datum->syntax #'a
(string->symbol (format "~a-~a"
(syntax->datum #'a)
(syntax->datum #'b))))])
#'(define (name args ...)
body0 body ...))]))
> (hyphen-define/ok2 foo bar () #t)
> (foo-bar)
#t
实际上, with-syntax
可以看成是一种syntax-case
的句法糖.(with-syntax ([<pattern> <syntax>]) <body>)
差不多等价于(syntax-case <syntax> () [<pattern> <body>])
所以这不是什么魔法!with-syntax*
with-syntax*
之于with-syntax
, 就像let*
之于let
.
> (require (for-syntax racket/syntax))
> (define-syntax (foo stx)
(syntax-case stx ()
[(_ a)
(with-syntax* ([b #'a]
[c #'b])
#'c)]))
读者需要注意的是, with-syntax*
并没有由racket/base
提供, 所以你需要(require (for-syntax racket/syntax))
. {译注: 原文所说的令人困惑的错误消息, 现在已经变了, 变得相当正常:> (define-syntax (foo stx)
(syntax-case stx ()
[(_ a)
(with-syntax* ([b #'a]
[c #'b])
#'c)]))
> (foo 0)
with-syntax*: undefined;
cannot reference an identifier before its definition
format-id
racket/syntax
里面存在着一个叫做format-id
的辅助函数, 其可以帮助我们更优雅地生成想要的标识符.
> (require (for-syntax racket/syntax))
> (define-syntax (hyphen-define/ok3 stx)
(syntax-case stx ()
[(_ a b (args ...) body0 body ...)
(with-syntax ([name (format-id #'a "~a-~a" #'a #'b)])
#'(define (name args ...)
body0 body ...))]))
> (hyphen-define/ok3 bar baz () #t)
> (bar-baz)
#t
这免去了之前我们所体验到的诸多繁琐.format-id
的一个参数是词法上下文, 一般来说读者应该不会想在这里填上stx
, 而会是更加特化的上下文信息, 例如这里是#'a
.
以下是一个变种, 其可以接受多个名称部分, 而将它们以连字号连接.
> (require (for-syntax racket/string racket/syntax))
> (define-syntax (hyphen-define* stx)
(syntax-case stx ()
[(_ (names ...) (args ...) body0 body ...)
(let ([name-stxs (syntax->list #'(names ...))])
(with-syntax ([name (datum->syntax (car name-stxs)
(string->symbol
(string-join (for/list ([name-stx name-stxs])
(symbol->string
(syntax-e name-stx)))
"-")))])
#'(define (name args ...)
body0 body ...)))]))
> (hyphen-define* (foo bar baz) (v) (* 2 v))
> (foo-bar-baz 50)
100
或许这里最值得注意的地方是我们提供给datum->syntax
的词法上下文参数是什么.struct
现在让我们应用我们所学到的东西于一个更为实际的例子. 我们将会实现一个类似于struct
的机制, 但远为简化.
对于以下的结构声明:
(our-struct name (field1 field2 ...))
我们需要定义以下的一些过程:vector
, 而结构名出现在第零位置.?
.struct
保持一致, 也就是结构名和field名之间以连字号连接.> (require (for-syntax racket/syntax))
> (define-syntax (our-struct stx)
(syntax-case stx ()
[(_ id (fields ...))
(with-syntax ([pred-id (format-id #'id "~a?" #'id)])
#`(begin
; Define a constructor.
(define (id fields ...)
(apply vector (cons 'id (list fields ...))))
; Define a predicate.
(define (pred-id v)
(and (vector? v)
(eq? (vector-ref v 0) 'id)))
; Define an accessor for each field.
#,@(for/list ([x (syntax->list #'(fields ...))]
[n (in-naturals 1)])
(with-syntax ([acc-id (format-id #'id "~a-~a" #'id x)]
[ix n])
#`(define (acc-id v)
(unless (pred-id v)
(error 'acc-id "~a is not a ~a struct" v 'id))
(vector-ref v ix))))))]))
; Test it out
> (require rackunit)
> (our-struct foo (a b))
> (define s (foo 1 2))
> (check-true (foo? s))
> (check-false (foo? 1))
> (check-equal? (foo-a s) 1)
> (check-equal? (foo-b s) 2)
> (check-exn exn:fail?
(lambda () (foo-a "furble")))
; The tests passed.
; Next, what if someone tries to declare:
> (our-struct "blah" ("blah" "blah"))
format-id: contract violation
expected: (or/c string? symbol? identifier? keyword? char?
number?)
given: #<syntax:eval:83:0 "blah">
这里的错误信息不是很有用, 因为其是来源于format-id
的, 算是一种实现细节.你可能听说过syntax-case
的语句可以包含一个可选的guard
或者说fender
表达式. 一个语句不仅可能是
[<pattern> <template>]
还可能是[<pattern> <guard> <template>]
让我们为our-struct
添加guard表达式.> (require (for-syntax racket/syntax))
> (define-syntax (our-struct stx)
(syntax-case stx ()
[(_ id (fields ...))
; Guard or "fender" expression:
(for-each (lambda (x)
(unless (identifier? x)
(raise-syntax-error #f "not an identifier" stx x)))
(cons #'id (syntax->list #'(fields ...))))
(with-syntax ([pred-id (format-id #'id "~a?" #'id)])
#`(begin
; Define a constructor.
(define (id fields ...)
(apply vector (cons 'id (list fields ...))))
; Define a predicate.
(define (pred-id v)
(and (vector? v)
(eq? (vector-ref v 0) 'id)))
; Define an accessor for each field.
#,@(for/list ([x (syntax->list #'(fields ...))]
[n (in-naturals 1)])
(with-syntax ([acc-id (format-id #'id "~a-~a" #'id x)]
[ix n])
#`(define (acc-id v)
(unless (pred-id v)
(error 'acc-id "~a is not a ~a struct" v 'id))
(vector-ref v ix))))))]))
; Now the same misuse gives a better error message:
> (our-struct "blah" ("blah" "blah"))
eval:86:0: our-struct: not an identifier
at: "blah"
in: (our-struct "blah" ("blah" "blah"))
之后, 我们将会看到syntax-parse
做类似的事情将会更加容易.之前的例子将一些标识符连接起来形成新的标识符, 而这里我们要做相反的事情: 将标识符拆成数个部分.
其他的语言中你经常可以看到点记法, 例如在JavaScript中使用JSON. 迭代使用点记法在Racket中的等价物往往是繁琐的, 例如
foo = js.a.b.c;
可能要写成(hash-ref (hash-ref (hash-ref js 'a) 'b) 'c)
或许我们可以编写一个辅助函数, 使得类似的事情变得更为容易和清晰.; This helper function:
> (define/contract (hash-refs h ks [def #f])
((hash? (listof any/c)) (any/c) . ->* . any)
(with-handlers ([exn:fail? (const (cond [(procedure? def) (def)]
[else def]))])
(for/fold ([h h])
([k (in-list ks)])
(hash-ref h k))))
; Lets us say:
> (hash-refs js '(a b c))
"value"
这已经是不错了, 但是或许我们还可以用宏做得更好.; This macro:
> (require (for-syntax racket/syntax))
> (define-syntax (hash.refs stx)
(syntax-case stx ()
; If the optional ‘default' is missing, use #f.
[(_ chain)
#'(hash.refs chain #f)]
[(_ chain default)
(let* ([chain-str (symbol->string (syntax->datum #'chain))]
[ids (for/list ([str (in-list (regexp-split #rx"\\." chain-str))])
(format-id #'chain "~a" str))])
(with-syntax ([hash-table (car ids)]
[keys (cdr ids)])
#'(hash-refs hash-table 'keys default)))]))
; Gives us "sugar" to say this:
> (hash.refs js.a.b.c)
"value"
; Try finding a key that doesn't exist:
> (hash.refs js.blah)
#f
; Try finding a key that doesn't exist, specifying the default:
> (hash.refs js.blah 'did-not-exist)
'did-not-exist
的确可行.现在和之前一样, 我们希望能够使得错误信息提供有用的提示.
> (require (for-syntax racket/syntax))
> (define-syntax (hash.refs stx)
(syntax-case stx ()
; Check for no args at all
[(_)
(raise-syntax-error #f "Expected hash.key0[.key1 ...] [default]" stx)]
; If the optional ‘default' is missing, use #f.
[(_ chain)
#'(hash.refs chain #f)]
[(_ chain default)
(unless (identifier? #'chain)
(raise-syntax-error #f "Expected hash.key0[.key1 ...] [default]" stx #'chain))
(let* ([chain-str (symbol->string (syntax->datum #'chain))]
[ids (for/list ([str (in-list (regexp-split #rx"\\." chain-str))])
(format-id #'chain "~a" str))])
; Check that we have at least hash.key
(unless (and (>= (length ids) 2)
(not (eq? (syntax-e (cadr ids)) '||)))
(raise-syntax-error #f "Expected hash.key" stx #'chain))
(with-syntax ([hash-table (car ids)]
[keys (cdr ids)])
#'(hash-refs hash-table 'keys default)))]))
; See if we catch each of the misuses
> (hash.refs)
eval:97:0: hash.refs: Expected hash.key0[.key1 ...]
[default]
in: (hash.refs)
> (hash.refs 0)
eval:98:0: hash.refs: Expected hash.key0[.key1 ...]
[default]
at: 0
in: (hash.refs 0 #f)
> (hash.refs js)
eval:99:0: hash.refs: Expected hash.key
at: js
in: (hash.refs js #f)
> (hash.refs js.)
eval:100:0: hash.refs: Expected hash.key
at: js.
in: (hash.refs js. #f)
还算可以, 但是这种错误处理在某种意义上有点恼人, 使得逻辑的主干部分不那么明晰.说到底, 很难说使用hash-refs
还是hash.refs
更加清晰, 但是至少Racket提供了这样的选择.
照应if
是一个流行的宏的例子. 例如, 我们可以将
(let ([tmp (big-long-calculation)])
(if tmp
(foo tmp)
#f))
写成(aif (big-long-calculation)
(foo it)
#f)
换言之, 当condition为真时, 标识符it
会被自动创建并置为condition之值. 似乎定义这个宏可能很简单:> (define-syntax-rule (aif condition true-expr false-expr)
(let ([it condition])
(if it
true-expr
false-expr)))
> (aif #t (displayln it) (void))
it: undefined;
cannot reference an identifier before its definition
in module: 'program
当然了, 这么简单不太可能.racket/splicing
的要义为何?syntax-parse
> (define (misuse s)
(string-append s " snazzy suffix"))
; User of the function:
> (misuse 0)
string-append: contract violation
expected: string?
given: 0
argument position: 1st
other arguments...:
" snazzy suffix"
; I guess I goofed, but – what is this "string-append" of which you
; speak??
> (define (misuse s)
(unless (string? s)
(error 'misuse "expected a string, but got ~a" s))
(string-append s " snazzy suffix"))
; User of the function:
> (misuse 0)
misuse: expected a string, but got 0
; I goofed, and understand why! It's a shame the writer of the
; function had to work so hard to tell me.
> (define/contract (misuse s)
(string? . -> . string?)
(string-append s " snazzy suffix"))
; User of the function:
> (misuse 0)
misuse: contract violation
expected: string?
given: 0
in: the 1st argument of
(-> string? string?)
contract from: (function misuse)
blaming: program
(assuming the contract is correct)
at: eval:131.0
; I goofed, and understand why! I'm happier, and I hear the writer of
; the function is happier, too.
#lang typed/racket
> (: misuse (String -> String))
> (define (misuse s)
(string-append s " snazzy suffix"))
> (misuse 0)
eval:3:0: Type Checker: type mismatch
expected: String
given: Zero
in: 0
对于宏而言, 我们也有着类似的选择.
syntax-parse
. 对于宏而言, 这相当于使用合同或者类型.syntax-parse
原作者其实还没有写这个部分就是了, 而是建议阅读Introduction.