【问题标题】:Broken While Loop Using Macros in Racket在 Racket 中使用宏破坏 While 循环
【发布时间】:2018-10-16 05:41:06
【问题描述】:

我正在使用 Racket 宏实现一个 while 循环。我的问题是,有人可以向我解释为什么下面的代码会产生无限宏扩展循环吗?递归 while 调用之前的最后一条语句 - body - 将 x 的值减 1,因此您会认为我们将朝着 x 将等于 0 的条件前进。但我显然遗漏了一些东西。提前谢谢!

(let ([x 5])
(while (> x 0)
     (displayln x)
     (set! x (- x 1))))    

 (define-syntax while
  (syntax-rules ()
    ((while c var body)
        (if
           c
          (begin
           var
           body
          (while c var  body))
          (void)))))

【问题讨论】:

  • 您的扩展包含您最初扩展的表达式,因此每个扩展都需要至少一个新的。您需要让表达式在结果中没有自己。也许命名为let

标签: loops macros racket


【解决方案1】:

宏不是函数。它们生成代码,它们在编译时应用,它们对运行时值一无所知。这意味着即使你这样写:

(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 编写没有赋值的迭代。不过,如果您只是在尝试使用宏系统,那是完全合理的。

【讨论】:

  • 非常感谢您的详细回答。我有一个后续问题。根据您的解释,是否可以公平地说 Racket 中的宏不应该递归使用,因为它们总是会表现出这种类型的行为(即在编译时无限期扩展)?
  • @AlexanderNenartovich 有递归宏很好,但就像递归函数一样,它们需要有一个基本情况,并且需要在编译时达到基本情况。例如,cond 最合乎逻辑的实现是将其递归转换为嵌套的if 形式,并且由于每个递归步骤处理一个子句,因此基本情况是空的(cond) 形式。
  • 是否可以使用递归宏重写我的尝试并添加一个可以在编译时达到的基本情况?只是好奇。
  • @AlexanderNenartovich 不,因为你的循环的基本情况是在运行时——在程序执行之前你不知道循环应该运行多少次。但是使用cond,您可以在编译时确切知道有多少子句(因为用户明确地编写了它们)。换句话说,编写递归 while 宏将执行循环 unrolling,您需要提前知道会发生多少次迭代。
  • 再次感谢您!在您使用命名 let 编写的解决方案中,如果条件 c 为真,循环是否充当程序从最后一行跳转到并执行主体的标签?这基本上是 Racket 写循环的方式吗?
猜你喜欢
  • 2020-06-10
  • 1970-01-01
  • 1970-01-01
  • 2018-03-21
  • 1970-01-01
  • 1970-01-01
  • 2020-10-04
  • 2018-05-05
  • 1970-01-01
相关资源
最近更新 更多