目录
前言
我在主要使用C和C++25年后学会了Racket。
结果是精神上的鞭打。
“所有的括号”实际上并不是什么大事。相反,第一个思维扭曲是函数编程。
没过多久,我就开始思考这个问题,并继续对Racket的许多其他方面和功能感
到舒适和有效。
但仍有两条最后的边界: 宏(Macors)和续延(continuations)。
我发现简单的宏很容易理解,而且还有很多好的教程。但当我跨过常规模式匹
配的那一刻,我有点跌入术语汤的悬崖。我把自己浸泡在材料中,希望在足够
多的重新阅读后,它最终会沉下去。我甚至发现自己在尝试和错误,而不是有
一个清晰的心理模型。
我开始写这篇文章的时候,形状正慢慢从雾中浮现出来。
我的主要动机是自私。解释一些东西迫使我更彻底地学习。此外,如果我写了
一些有错误的东西,其他人会急于指出并纠正我。这是元编程的社会工程变体
吗?请回答下一个问题。
最后,我确实希望它能帮助其他和我有相似背景和/或学习风格的人。
我想展示Racket宏功能如何演变为解决问题或烦恼的解决方案。当我发现一个
已经存在的问题的答案,或者找到一个我已经感到痛苦的问题的解决方案时,
我会更快更深入地学习。因此,我将首先向您提出问题(questions)和问题
(problems),以便您更好地理解(appreciate)和理解(understand)答案
(answers)和解决方案(solutions)。
我们的进攻计划
您最希望用于生产质量宏的宏系统称为: syntax-parse
。别担心,我们很
快就会做到的。
但如果我们从这里开始,你可能会被概念和术语淹没,并感到非常困惑。我曾经也是这样的。
- 相反,让我们从基础开始: 一个语法对象和一个改变它的函数——“转换器”。
我们将在这一层面上工作一段时间,以适应并消除整个宏业务的神话色彩。 - 很快我们就会意识到模式匹配会让生活更轻松。我们将学习
syntax-case
及其缩写define-syntax-rules
。我们会发现,如果我
们想在将模式变量重新粘贴到模板中之前咀嚼(munge)模式变量,我们可能
会感到困惑,并学习如何做到这一点。 - 此时,我们将能够编写许多有用的宏。但是,如果我们想用一个“神奇的
变量”来写一个流行的回指“if”呢?事实证明,我们受到了保护,不会
犯某些错误。当我们有意做这种事情时,我们使用语法参数。[还有其他更
古老的方法可以做到这一点。我们不会去看它们。我们也不会花很多时间
提倡“卫生”——我们只会规定这是好的。] - 最后,我们会意识到,当宏被错误使用时,它们可能会更聪明。正常的
Racket函数可以选择具有契约和类型。这些捕获使用错误并提供清晰、有
用的错误消息。如果宏有类似的东西,那就太好了。有。最新的Racket宏
增强功能之一是syntax-parse
。
转换
YOU ARE INSIDE A ROOM.
THERE ARE KEYS ON THE GROUND.
THERE IS A SHINY BRASS LAMP NEARBY.
IF YOU GO THE WRONG WAY, YOU WILL BECOME
HOPELESSLY LOST AND CONFUSED.
> pick up the keys
YOU HAVE A SYNTAX TRANSFORMER
什么是语法转换器
语法转换器不是トランスフォーマ 转换器。
相反,它只是一个函数。该函数接受语法并返回语法。它转换语法。
这里有一个转换器函数,它忽略其输入语法,并始终输出字符串文本的语法:
(define-syntax foo
(lambda (stx)
(syntax "I am foo")))
使用它:
> (foo)
"I am foo"
当我们使用 define-syntax
时,我们正在制作转换器绑定。这告诉Racket
编译器,“每当您遇到以foo开头的语法块时,请将其交给我的转换函数,并
用我给您的语法替换它。”所以Racket将为我们的函数提供任何类似(foo…)
的内容,我们可以返回新的语法来代替。很像搜索和替换。
也许你知道在Racket中定义函数的常用方法:
(define (f x) ...)
简称:
(define f (lambda (x) ...))
这种速记方式可以避免键入lambda和一些括号。
define-syntax
有一个类似的缩写:
(define-syntax (also-foo stx)
(syntax "I am also foo"))
;; use it:
> (also-foo)
"I am also foo"
我们要记住的是,这只是一种速记。我们仍然在定义一个转换器函数,它接
受语法并返回语法。我们对宏所做的一切都将建立在这个基本思想之上。这
不是魔法。
说到速记,还有一种语法的速记,它是 #'
:
(define-syntax (quoted-foo stx)
#'"I am also foo, using #' instead of syntax")
> (quoted-foo)
"I am also foo, using #' instead of syntax"
从现在起,我们将使用 #'
速记。
当然,我们可以发出比字符串文字更有趣的语法。返回 (display "hi")
怎么样?
(define-syntax (say-hi stx)
#'(display "hi"))
> (say-hi)
当Racket扩展我们的程序时,它会看到 (say-hi)
, 并看到它具有转换器函
数。它用旧语法调用我们的函数,我们返回新的语法,用于求值和运行我们
的程序。
输入是什么?
到目前为止,我们的示例忽略了输入语法,并输出了一些固定语法。但通常
我们需要将输入语法转换为其他语法。
让我们从仔细观察实际输入内容开始:
(define-syntax (show-me stx)
(print stx)
#'(void))
> (show-me '(+ 1 2))
#<syntax:stdin:23:0 (show-me (quote (+ 1 2)))>
(print stx)
显示了给我们的转换器提供什么:语法对象。
语法对象由几部分组成。第一部分是表示代码的S表达式,例如 '(+1 2)
。
Racket语法也有一些有趣的信息,如源文件、行号和列。最后,它有关于词
汇范围的信息(您现在不必担心,但稍后会发现这很重要)
有多种函数可用于访问语法对象。让我们定义一段语法:
> (define stx #'(if x (list "true") #f))
> stx
#<syntax:stdin:27:14 (if x (list "true") #f)>
现在让我们使用访问语法对象的函数。源信息函数包括:
> (syntax-source stx)
'stdin
> (syntax-line stx)
27
> (syntax-column stx)
14
更有趣的是语法“stuff”本身。=syntax->datum= 将其完全转换为S表达式:
> (syntax->datum stx)
'(if x (list "true") #f)
而 syntax-e
只会“下降一级”。它可能返回一个包含语法对象的列表:
> (syntax-e stx)
'(#<syntax:stdin:27:15 if>
#<syntax:stdin:27:18 x>
#<syntax:stdin:27:20 (list "true")>
#<syntax:stdin:27:34 #f>)
这些语法对象中的每一个都可以通过 syntax-e
进行递归转换,这就是
syntax->datum
所做的。
在大多数情况下, syntax->list
给出的结果与 syntax-e
相同:
(syntax->list stx)
> '(#<syntax:stdin:27:15 if>
#<syntax:stdin:27:18 x>
#<syntax:stdin:27:20 (list "true")>
#<syntax:stdin:27:34 #f>)
syntax-e
和 syntax->list
什么时候会不同?我们现在不要偏离正轨。
当我们想要转换语法时,我们通常会把我们得到的片段取下来,也许会重新
排列它们的顺序,也许会改变其中的一些片段,并经常引入全新的片段。
实际转换输入
让我们编写一个转换器函数,将其语法反转:
(define-syntax (reverse-me stx)
(datum->syntax stx (reverse (cdr (syntax->datum stx)))))
> (reverse-me "backwards" "am" "i" values)
"i"
"am"
"backwards"
理解尤达,我们可以。太好了,但这是如何工作的?
首先,我们获取输入语法,并将其赋予syntax->datum。这将语法转换为简单
的旧列表:
> (syntax->datum #'(reverse-me "backwards" "am" "i" values))
'(reverse-me "backwards" "am" "i" values)
使用 cdr
将列表的第一项切掉,=reverse-me=,留下剩余部分:
("backwards" "am" "i" values)
。将其传递给 reverse
将其更改为
=(values “i” “am” “backwards”):
> (reverse (cdr '(reverse-me "backwards" "am" "i" values)))
'(values "i" "am" "backwards")
最后,我们使用 datum->syntax
将其转换回语法:
> (datum->syntax #f '(values "i" "am" "backwards"))
#<syntax (values "i" "am" "backwards")>
这就是我们的转换器函数返回给Racket编译器的内容,并对语法进行评估:
> (values "i" "am" "backwards")
"i"
"am"
"backwards"
编译时间与运行时间
(define-syntax (foo stx)
(make-pipe)
#'(void))
正常的Racket代码在…运行时运行。对。
但是,语法转换器被Racket调用作为解析、扩展和编译程序过程的一部分。
换句话说,我们的语法转换器函数在编译时进行评估。
宏的这一方面使您可以做在正常代码中根本不可能做的事情。经典的例子之
一是类似于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)
(display x)
(newline)
x)
> (our-if #t
(display-and-return "true")
(display-and-return "false"))
true
false
"true"
哎呀。因为表达式有副作用,很明显它们都被求值了。这可能是一个问题,
如果副作用包括删除磁盘上的文件怎么办?您不希望 (if user-wants-file-deleted? (delete-file) (void))
删除文件,即使
user-wants-file-deleted?
是#f。
所以这不能作为一个简单的函数。然而,语法转换器可以在编译时重新排列
语法——重写代码。语法片段被四处移动,但直到运行时才真正被求值。
以下是一种方法:
(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"))
> (display stx)
#<syntax:stdin:90:14 (our-if-v2 #t "true" "false")>
-
我们采用原始语法,并使用
syntax->list
将其更改为语法对象列表:> (define xs (syntax->list stx)) > (display xs) (#<syntax:stdin:90:15 our-if-v2> #<syntax:stdin:90:25 #t> #<syntax:stdin:90:28 "true"> #<syntax:stdin:90:35 "false">)
-
要将其更改为Racket
cond
形式,我们需要使用cadr
、caddr
和cadddr
从列表中提取三个感兴趣的部分:条件、真表达式和假表达
式,并将它们排列成cond
形式:`(cond [,(cadr xs) ,(caddr xs)] [else ,(cadddr xs)])
-
最后,我们使用
datum->syntax
将其转换为语法:> (datum->syntax stx `(cond [,(cadr xs) ,(caddr xs)] [else ,(cadddr xs)])) #<syntax (cond (#t "true") (else "false"))>
所以这是有效的,但是使用cadddr等来解构列表是痛苦的,并且容易出错。
也许你知道Racket的match
?使用它可以让我们进行模式匹配。而不是:
> (define-syntax (our-if-v2 stx) (define xs (syntax->list)) (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: top-level
; [,bt for context]
哎呀。它抱怨没有定义 match
。
我们的转换函数在编译时工作,而不是运行时。在编译时,你只自动需要
racket/base
,而不是完整的 racket
。
任何超出 racket/base
的东西,我们都必须自己要求,并在编译时使用
require
的 for-syntax
形式来要求它。
在这种情况下,不使用普通 (require racket/base)
,,我们想要
(require (for-syntax racket/match))
—— 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" "fase")
"true"
快乐。
begin-for-syntax
我们使用 for-syntax
来 require
racket/match
模块,因为我们需
要在编译时使用 match
。
如果我们想定义宏使用的帮助函数,该怎么办?一种方法是将其放在另一个
模块中,并使用 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
,它组合了
begin-for-syntax
和 define
:
(define-for-syntax (my-helper-function ...)
...)
(define-syntax (macro-using-my-helper-function sts)
(my-helper-function ...)
...)
回顾:
- 语法转换器在编译时工作,而不是运行时。好消息是,这意味着我们可以
在不评估语法的情况下重新排列语法。我们可以实现if
这样的形式,
它不能作为运行时函数正常工作。 - 更好的消息是,没有什么特别的、奇怪的语言用来编写语法转换器。我们
可以使用我们已经知道并喜爱的Racket语言来编写这些转换器函数。 - 半坏消息是,这种熟悉会让人很容易忘记我们不是在运行时工作。有时候,
记住这一点很重要。- 例如只有
racket/base
是自动require
的,如果我们需要其他模
块,我们必须require
它们,并且在编译时使用for-syntax
。 - 类似地,如果我们想在与使用它们的宏相同的文件/模块中定义助手函数,
我们需要将定义包装在begin-for-syntax
形式中。这样做可以使它
们在编译时可用。
- 例如只有
模式匹配: syntax-case
和 syntax-rules
大多数有用的语法变换器通过采用一些输入语法,并将其重新排列为其他语法
来工作。正如我们所看到的,这是可能的,但使用列表访问器(如cadddr)会
很乏味。使用match进行模式匹配更方便,也不容易出错。
事实证明,模式匹配是Racket宏系统中添加的首批改进之一。它被称为
syntax-case
,有一个简单情况的缩写,称为 define-syntax-rule
。
回想我们之前的例子:
(require (for-syntax racket/match))
(define-syntax (out-if-using-match-v2 stx)
(match (syntax->list stx)
[(list _ condition true-expr false-expr)
(datum->syntax sts `(cond [,condition ,true-expr]
[else ,false-expr]))]))
下面是使用 syntax-case
时的样子:
(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
。相
反,我们提供了一个“模板”,它使用模式中的变量。
有一个简单模式匹配案例的简写,它扩展为 syntax-case
。它被称为 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"
这是关于 define-syntax-rule
的事情。因为它非常简单,所以
define-syntax-rule
通常是人们学习宏的第一件事。但是它几乎简单得令
人难以置信。它看起来很像定义一个正常的运行时函数,但事实并非如此。它
在编译时工作,而不是运行时。更糟糕的是,当你想做的事情超出
defing-syntax-rule
所能处理的范围时,你可能会从悬崖上跌落到一个复
杂而令人困惑的领域。希望,因为我们从一个基本的语法转换器开始,并从它
开始工作,所以我们不会遇到这个问题。我们可以理解将
define-syntax-rule
作为一种方便的速记,但不要害怕或混淆它的速记。
我找到的大多数学习宏的材料,包括Racket指南,都很好地解释了模式和模板
是如何工作的。所以我不会在这里重复。
有时,我们需要超越模式和模板。让我们看看一些例子,我们如何会感到困惑,
以及如何让它发挥作用。
模式变量与模板之争!
假设我们想定义一个带有连字符名称的函数(a-b),但我们分别提供a和b部分。
Racket struct
宏执行如下操作: (struct foo (field1 field2))
自
动定义多个函数,其名称是名称foo的变体,例如 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 ...))]))
; stdin:163:51: a: pattern variable cannot be used outside of a template
; in: a
; [,bt for context]
呵呵。我们不知道这个错误消息意味着什么。好吧,让我们试着解决它。错
误消息所指的“模板”是 #'(define (name args ...) body0 body ...)
部分。 let
不是模板的一部分。听起来我们不能在 let
部分使用a(或
b)。
事实上, syntax-case
可以有任意多的模板。显然,所需的模板是提供输出语法
的最终表达式。但您可以在模式变量上使用 syntax
(又名#')。这使得另一个
模板,尽管是一个小的“有趣的大小”模板。让我们试试看:
> (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: top-level
; [,bt for context]
显然,我们的宏定义了一个函数,而不是 foo-bar
。呵呵。
这就是DrRacket中的宏步进器的价值所在。
宏步进器表示,我们的宏的使用:
(hyphen-define/wrong1.1 foo bar () #t)
扩展到:
(define (name) #t)
这就是为什么。相反,我们希望扩展到:
(define (foo-bar) #t)
我们的模板使用的是符号 name
,但我们想要它的值,例如在宏的使用中
的 foo-bar
。
我们是否已经知道,在模板中使用变量会产生这样的值?是:模式变量。我
们的模式不包括 name
,因为我们不希望它在原始语法中出现。实际上,
这个宏的全部目的是创建它。所以 name
不能出现在主模式中。好吧,让
我们再做一个模式。我们可以使用一个额外的嵌套 syntax-case
来实现这
一点:
(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 ...)])]))
看起来很奇怪?让我们深呼吸。通常,我们的转换器函数由Racket给出语法,
我们将语法传递给 syntax-case
。但我们也可以在运行中创建一些自己的
语法,并将其传递给 syntax-case
。这就是我们在这里所做的一切。整个
(datum->syntax ...)
表达式是我们正在动态创建的语法。我们可以将其
指定为 syntax-case
,并使用名为 name
的模式变量进行匹配。瞧,我
们有一个新的模式变量。我们可以在模板中使用它,它的值将在模板中。
我保证,我们可能会留下再多一个小问题。让我们尝试使用我们的新版本:
> (hyphen-define/wrong1.2 foo bar () #t)
> (foo-bar)
; foo-bar: undefined;
; cannot reference an identifier before its definition
; in module: top-level
; [,bt for context]
嗯, foo-bar
仍然没有定义。返回宏步进器。它说,现在我们正在扩展到:
(define (|#<syntax:unsaved-editor:12:24 foo>-#<syntax:unsaved-editor:12:28 bar>|)
对了:#'a和#'b是语法对象。因此
(string->symbol (format "~a-~a" #'a #'b))
是两个语法对象的打印形式,由连字符连接:
|#<syntax:11:24foo>-#<syntax:11:28 bar>|
相反,我们需要语法对象中的数据,例如符号foo和bar。我们使用
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
。这将
重新排列 syntax-case
,使其看起来更像 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)
#t
同样, with-syntax
只是 syntax-case
的重新排列:
(syntax-case <syntax> () [<pattern> <body>])
(with-syntax ([<pattern> <syntax>]) <body>)
无论您是使用附加的 syntax-case
还是使用 with-syntax
,无论哪种
方式,您都只是简单地定义附加的模式变量。不要让术语和结构让它看起来
很神秘。
with-syntax*
我们知道, let
不允许我们在后续绑定中使用绑定:
> (let ([a 0]
[b a])
b)
; a: undefined;
; cannot reference an identifier before its definition
; in module: top-level
; [,bt for context]
相反,我们可以嵌套let:
> (let ([a 0])
(let ([b a])
b))
0
或者使用嵌套的速记, let*
:
> (let* ([a 0]
[b a])
b)
0
类似地,我们可以使用 with-syntax*
来代替嵌套 with-syntax
编写:
> (require (for-syntax racket/syntax))
> (define-syntax (foo stx)
(syntax-case stx ()
[(_ a)
(with-syntax* ([b #'a]
[c #'b])
#'c)]))
(foo 'a)
'a
有一个问题是, racket/base
没有提供 with-syntax*
。我们必须
require
(racket/syntax)
。否则,我们可能会收到一条令人困惑的
错误消息:
; with-syntax*: undefined;
; cannot reference an identifier before its definition
; in module: top-level
; [,bt for context]
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
很方便,因为它可以处理从语法到符号数据到字符串的繁琐转换。
format-id
的第一个参数, lctx
是将创建的标识符的词汇上下文。您
几乎永远不想向 stx
提供宏转换的整个语法块。相反,您需要提供一些
更具体的语法,例如用户提供给宏的标识符。在本例中,我们使用 #'a
。生成的标识符将具有与用户提供的标识符相同的范围。这更有可能像用户
期望的那样,尤其是当我们的宏与其他宏组合时。
(译者注: 因为 lctx
提供了与用户提供的标识符相同的范围,所以在使
用 syntax->datum
访问 #'a
或 #'b
时可以取到对应语法对象的数
据。)
另一例子
最后,这里有一个变体,它接受任意数量的名称部分以连字符连接:
> (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
就像我们使用 format-id
时一样,当使用 datum->syntax
时,我们要
小心第一个参数 lctx
。我们希望创建的标识符使用用户提供给宏的标识
符的词汇上下文。在这种情况下,用户的标识符位于 (names ...)
模板
变量中。我们将其从一个语法更改为一个语法列表。我们使用词汇上下文的
第一个元素。当然,我们将使用所有元素来形成连字符标识符。
回顾:
- 不能在模板之外使用模式变量。但您可以在模式变量上使用语法或#'来制
作一个临时的“有趣的大小”模板。 - 如果你想在模板中使用模式变量,
with-syntax
是你的朋友,因为它
允许你创建新的模式变量。 - 通常,您需要使用
syntax->datum
来获取内部有趣的值。 format-id
便于格式化标识符名称。
制作我们自己的结构
让我们将刚刚学到的知识应用到一个更现实的例子中。我们将假设Racket还
没有 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 construtor (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)
(out-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:interactions from an unsaved editor:20:14 “blah”>
错误消息不是很有用。它来自 format-id
,它是我们宏的私有实现细节。
您可能知道, syntax-case
子句可以采用可选的“guard”或“fender”
表达式。而不是
[pattern template]
它可以是:
[pattern guard template]
让我们在子句中添加一个保护表达式:
(require (for-syntax racket/syntax))
(define-syntax (our-struct stx)
(syntax-case stx ()
[(_ id (fields ...))
;; Guard or "feader" 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 (accd-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"))
; stdin:625:12: our-struct: not an identifier
; at: "blah"
; in: (our-struct "blah" ("blah" "blah"))
; [,bt for context]
稍后,我们将看到 syntax-parse
如何使检查用法更加容易,并提供有关
错误的有用消息。
使用点符号进行嵌套哈希查找
前两个示例使用宏来定义函数,这些函数的名称是通过连接提供给宏的标识
符来实现的。这个例子做了相反的事情:给宏的标识符被分割成多个部分。
如果您为web服务编写程序,您将处理JSON,JSON在Racket中由 jsexpr?
表示。JSON通常有包含其他字典的字典。在 jsexpr?
中这些由嵌套的哈希
表表示:
;;; Nestd 'hasheq's typical of a jsexpr:
> (define js (hasheq 'a (hasheq 'b (hasheq 'c "value"))))
在JavaScript中,您可以使用点符号:
foo = js.a.b.c
在Racket中不太方便:
(hash-ref (hash-ref (hash-ref js 'a) 'b) 'c)
我们可以编写一个helper函数,使其更简洁:
;; 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))
这样更好。我们是否可以更进一步,使用点符号,有点像JavaScript?
;;; 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)
; stdin:62:0: hash.refs: Expected hash.key0[.key1 ...] [default]
; in: (hash.refs)
; [,bt for context]
> (hash.refs 0)
; stdin:63:11: hash.refs: Expected hash.key0[.key1 ...] [default]
; at: 0
; in: (hash.refs 0 #f)
; [,bt for context]
> (hash.refs js)
; stdin:64:11: hash.refs: Expected hash.key
; at: js
; in: (hash.refs js #f)
; [,bt for context]
> (hash.refs js.)
; stdin:65:11: hash.refs: Expected hash.key
; at: js.
; in: (hash.refs js. #f)
; [,bt for context]
还不错。当然,带有错误检查的版本要长一些。错误检查代码通常会模糊逻
辑,在这里也是如此。幸运的是,我们很快就会看到 syntax-parse
如何
帮助缓解这种情况,就像普通Racket中的契约或Typed Racket的类型一样。
也许我们不相信写 (hash-refs js.a.b.c)
比写 (hash-refs js '(a b c))
更清楚。也许我们不会真正使用这种方法。但Racket宏系统使其成为可
能的选择。
语法参数
“回指if”或“aif”是一个流行的宏示例。代替写作:
(let ([tmp (big-long-calculation)])
(if tmp
(foo tmp)
#f))
你可以写:
(aif (big-long-calculation)
(foo it)
#f)
换句话说,当条件为真时,将自动创建一个 it
标识符,并将其设置为条件
的值。这应该很容易:
> (define-syntax-rule (aif condition true-expr false-expr)
(let ([it condition])
(if it
true-expr
false-expr)))
> (aif #t (display it) (void))
; it: undefined;
; cannot reference an identifier before its definition
; in module: top-level
; [,bt for context]
等等,什么? it
是未定义的?
事实证明,一直以来,我们都受到保护,不会在宏中犯某种错误。错误是,如
果我们的新语法引入了一个变量,该变量与宏周围的代码中的变量意外冲突。
Racket参考部分“转换器绑定”有一个很好的解释和示例。基本上,语法有
“标记”来保留词汇范围。这使宏在词汇范围内表现得像一个普通函数。
如果一个普通函数定义了一个名为x的变量,那么它不会与外部作用域中名为x
变量冲突:
> (let ([x "outer"])
(let ([x "inner"])
(printf "The inner `x' is ~s\n" x))
(printf "The outer `x' is ~s\n" x))
当我们的宏也尊重词法范围时,更容易编写行为可预测的可靠宏。
所以这是一个很好的默认行为。但有时我们想故意引入一个神奇的变量,比如
aif
。
这有一个坏办法,也有一个好办法。
坏的方法是使用 datum->syntax
,这很难正确使用。
最好的方法是使用语法参数,使用 define-syntax-parameter
和
syntax-parameterize
。您可能熟悉Racket中的常规参数:
> (define current-foo (make-parameter "some default value"))
> (current-foo)
"some default value"
> (parameterize ([current-foo "I have a new value, for now"])
(current-foo))
"I have a new value, for now"
> (current-foo)
"some default value"
这是一个正常的参数。语法变体的工作原理类似。我们的想法是,我们将定义
it
为默认情况下的错误。只有在我们的aif内部,它才会有意义的价值:
> (require racket/stxparam)
> (define-syntax-parameter it
(lambda (stx)
(raise-syntax-error (syntax-e stx) "can only be used inside aif")))
> (define-syntax-rule (aif condition true-expr false-expr)
(let ([tmp condition])
(if tmp
(syntax-parameterize ([it (make-rename-transformer #'tmp)])
true-expr)
false-expr)))
>(aif 10 (display it) (void))
10
>(aif #f (display it) (void))
在 syntax-parameterize
中,它充当 tmp
的别名。别名行为由
make-rename-transformer
创建。
如果我们尝试在 aif
形式之外使用它,并且它没有另外定义,我们会得到
一个错误,就像我们想要的那样:
> (display it)
; it: can only be used inside aif [,bt for context]
但我们仍然可以在局部定义上下文中将其定义为正常变量,如:
> (let ([it 10])
it)
;; or
> (define (foo)
(define it 10)
it)
(foo)
有关更深入的了解,请参见使用语法参数保持干净。
参考文章
仅供参考,如有帮助不胜荣幸,如需转载请注明出处。
请关注、点赞、收藏。