我对宏在 Scheme 中的工作方式有一些疑问(特别是在 Chicken Scheme 中),让我们考虑一下这个例子:
(define (when-a condition . body)
(eval `(if ,condition
(begin ,@body)
'())))
(define-syntax when-b
(er-macro-transformer
(lambda (exp rename compare)
(let ((condition (cadr exp))
(body (cddr exp)))
`(if ,condition (begin ,@body) '())))))
(define-syntax when-c
(ir-macro-transformer
(lambda (exp inject compare)
(let ((condition (cadr exp))
(body (cddr exp)))
`(if ,condition (begin ,@body) '())))))
(define-syntax when-d
(syntax-rules ()
((when condition body ...)
(if condition (begin body ...) '()))))
我可以考虑
when-a
宏吗?我觉得我不能严格地将其视为宏,因为我没有使用define-syntax
但我无法说出任何实际的理由来不喜欢这种实现。我的宏卫生吗?
when-b
和when-c
有什么区别吗?由于我没有使用rename
也没有使用inject
我认为没有。
我可以考虑何时宏吗?我觉得我不能严格地将其视为宏,因为我没有使用定义语法,但我无法说出任何实际的理由来不喜欢这种实现。
这类似于宏,但它与真正的宏不完全相同,原因如下:
- 真正的宏和基于 eval 的"宏"之间的主要区别在于,您的方法将在调用它之前评估其所有参数。 这是一个非常重要的区别。例如:
(if #f (error "oops") '())
的计算结果为'()
,但(when-a #f (error "oops"))
将引发错误。 - 这不卫生。例如,在此之前,人们可能会做类似
(eval '(define if "not a procedure"))
的事情,这意味着这个评估会失败;"扩展"的正文表达中的if
并不是指定义站点的if
。 - 它在编译时不会展开。这是使用宏的另一个主要原因;编译器将扩展它,并且在运行时不会执行任何计算来执行扩展。宏本身将完全蒸发。只剩下扩展。
我的宏卫生吗?
只有when-c
和when-d
,因为ir-macro-transformer
和syntax-rules
的保证。在when-b
中,您必须重命名if
和begin
,以使它们引用宏定义站点上的if
和begin
版本。
例:
(let ((if #f))
(when-b #t (print "Yeah, ok")))
== expands to ==>
(let ((if1 #f))
(if1 #t (begin1 (print "Yeah, ok"))))
这将失败,因为两个版本的if
(此处用额外的1
后缀注释)都引用了相同的内容,因此我们最终将在运算符位置调用#f
。
相比之下,
(let ((if #f))
(when-c #t (print "Yeah, ok")))
== expands to ==>
(let ((if1 #f))
(if2 #t (begin1 (print "Yeah, ok"))))
这将按预期工作。 如果你想重写when-b
以保持卫生,请这样做:
(define-syntax when-b
(er-macro-transformer
(lambda (exp rename compare)
(let ((condition (cadr exp))
(body (cddr exp))
(%if (rename 'if))
(%begin (rename 'begin)))
`(,%if ,condition (,%begin ,@body) '())))))
请注意额外的%
前缀标识符,这些标识符引用if
的原始值,并begin
因为它们位于宏的定义位置。
when-b 和 when-c 之间有什么区别吗?由于我没有使用重命名或注入,我认为没有。
有。之所以这样调用隐式重命名宏,是因为它们隐式重命名来自使用站点的所有标识符,以及您在正文中引入的每个新标识符。如果注入任何标识符,则会撤消此隐式重命名,从而使调用代码无法不卫生地捕获它们。
另一方面,调用显式重命名宏是因为您必须显式重命名任何标识符以防止它们被调用代码捕获。