宏不是函数。它们生成代码,它们在编译时应用,它们对运行时值一无所知。这意味着即使你这样写:
(define-syntax-rule (some-macro)
(displayln "hello!"))
(when #f
(some-macro))
……some-macro的使用还是会被扩展,程序会变成这个:
(when #f
(displayln "hello!"))
在使用宏时理解这一点非常重要:宏实际上是代码替换工具。扩展宏时,宏的使用实际上被宏生成的代码所取代。这可能会导致递归宏出现问题,因为如果您不小心,宏扩展将永远不会终止。考虑这个示例程序:
(define-syntax-rule (recursive-macro)
(when #f
(displayln "hello!")
(recursive-macro)))
(recursive-macro)
经过一个宏扩展步骤,(recursive-macro) 的使用将扩展为:
(when #f
(displayln "hello!")
(recursive-macro))
现在,如果recursive-macro 是一个函数,那么它出现在when 表单中的事实并不重要——它根本不会被执行。但是recursive-macro 不是一个函数,它是一个宏,它会被扩展而不管分支在运行时永远不会被采用。这意味着在第二个宏扩展步骤之后,程序将转换为:
(when #f
(displayln "hello!")
(when #f
(displayln "hello!")
(recursive-macro)))
我想你可以看到这是怎么回事。嵌套的recursive-macro 使用永远不会停止扩展,程序会迅速无限大。
您可能对此不满意。鉴于分支将永远被占用,为什么扩展器愚蠢地继续扩展?好吧,recursive-macro 是一个非常愚蠢的宏,因为编写一个扩展为永远不会执行的代码的宏并不是很有用。相反,假设我们稍微修改了recursive-macro 的定义:
(define-syntax-rule (recursive-macro)
(when (zero? (random 2))
(displayln "hello!")
(recursive-macro)))
现在很明显,扩展器无法知道递归调用将被执行多少次,因为行为是随机的,并且每次程序执行时生成的随机数都会不同。鉴于扩展是在编译时发生的,而不是在运行时,扩展器尝试并考虑运行时信息是没有意义的。
这是您的 while 宏的问题所在。您似乎期望 while 的行为类似于函数调用,并且在未采用的运行时分支中递归使用 while 不会被扩展。这是不正确的:宏在编译时扩展,而不考虑运行时信息。实际上,您应该将宏扩展过程视为一种转换,它生成一个没有任何宏的程序作为输出,然后才被执行。
考虑到这一点,您该如何解决?好吧,在编写宏时,您需要将自己视为编写一个小型编译器:您的宏需要通过将输入代码转换为执行所需行为的代码来实现其功能,完全定义为更简单的语言功能。在这种情况下,在 Racket 中实现循环的一种非常简单的方法是a named-let loop,就像这样:
(let ([x 5])
(let loop ()
(when (> x 0)
(displayln x)
(set! x (- x 1))
(loop))))
这使得实现while 构造变得容易:您只需编写一个将while 转换为等效的命名-let 的宏:
(define-syntax-rule (while c body ...)
(let loop ()
(when c
body ...
(loop))))
这会如你所愿。
当然,这个宏不是很惯用的 Racket。敲诈者更喜欢避免set! 和其他形式的突变,他们只会使用one of the built-in for constructs 编写没有赋值的迭代。不过,如果您只是在尝试使用宏系统,那是完全合理的。