【Scheme 系列】对宏(macros)的恐惧

目录

  1. 前言
  2. 我们的进攻计划
  3. 转换
    1. 什么是语法转换器
    2. 输入是什么?
    3. 实际转换输入
    4. 编译时间与运行时间
    5. begin-for-syntax
  4. 模式匹配: syntax-casesyntax-rules
    1. 模式变量与模板之争!
      1. with-syntax
      2. with-syntax*
      3. format-id
      4. 另一例子
    2. 制作我们自己的结构
    3. 使用点符号进行嵌套哈希查找
  5. 语法参数
  6. 参考文章

前言

我在主要使用C和C++25年后学会了Racket。

结果是精神上的鞭打。

“所有的括号”实际上并不是什么大事。相反,第一个思维扭曲是函数编程。
没过多久,我就开始思考这个问题,并继续对Racket的许多其他方面和功能感
到舒适和有效。

但仍有两条最后的边界: 宏(Macors)和续延(continuations)。

我发现简单的宏很容易理解,而且还有很多好的教程。但当我跨过常规模式匹
配的那一刻,我有点跌入术语汤的悬崖。我把自己浸泡在材料中,希望在足够
多的重新阅读后,它最终会沉下去。我甚至发现自己在尝试和错误,而不是有
一个清晰的心理模型。

我开始写这篇文章的时候,形状正慢慢从雾中浮现出来。

我的主要动机是自私。解释一些东西迫使我更彻底地学习。此外,如果我写了
一些有错误的东西,其他人会急于指出并纠正我。这是元编程的社会工程变体
吗?请回答下一个问题。

最后,我确实希望它能帮助其他和我有相似背景和/或学习风格的人。

我想展示Racket宏功能如何演变为解决问题或烦恼的解决方案。当我发现一个
已经存在的问题的答案,或者找到一个我已经感到痛苦的问题的解决方案时,
我会更快更深入地学习。因此,我将首先向您提出问题(questions)和问题
(problems),以便您更好地理解(appreciate)和理解(understand)答案
(answers)和解决方案(solutions)。

我们的进攻计划

您最希望用于生产质量宏的宏系统称为: syntax-parse 。别担心,我们很
快就会做到的。

但如果我们从这里开始,你可能会被概念和术语淹没,并感到非常困惑。我曾经也是这样的。

  1. 相反,让我们从基础开始: 一个语法对象和一个改变它的函数——“转换器”。
    我们将在这一层面上工作一段时间,以适应并消除整个宏业务的神话色彩。
  2. 很快我们就会意识到模式匹配会让生活更轻松。我们将学习
    syntax-case 及其缩写 define-syntax-rules 。我们会发现,如果我
    们想在将模式变量重新粘贴到模板中之前咀嚼(munge)模式变量,我们可能
    会感到困惑,并学习如何做到这一点。
  3. 此时,我们将能够编写许多有用的宏。但是,如果我们想用一个“神奇的
    变量”来写一个流行的回指“if”呢?事实证明,我们受到了保护,不会
    犯某些错误。当我们有意做这种事情时,我们使用语法参数。[还有其他更
    古老的方法可以做到这一点。我们不会去看它们。我们也不会花很多时间
    提倡“卫生”——我们只会规定这是好的。]
  4. 最后,我们会意识到,当宏被错误使用时,它们可能会更聪明。正常的
    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-esyntax->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")>
  1. 我们采用原始语法,并使用 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">)
    
  2. 要将其更改为Racket cond 形式,我们需要使用 cadrcaddr
    cadddr 从列表中提取三个感兴趣的部分:条件、真表达式和假表达
    式,并将它们排列成 cond 形式:

    `(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"))>
    

    所以这是有效的,但是使用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 的东西,我们都必须自己要求,并在编译时使用
requirefor-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-syntaxrequire 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-syntaxdefine

(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-casesyntax-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)

有关更深入的了解,请参见使用语法参数保持干净。

参考文章

  1. Fear of Macros

仅供参考,如有帮助不胜荣幸,如需转载请注明出处。
请关注、点赞、收藏。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值