【问题标题】:For-loop macro in RacketRacket 中的 For 循环宏
【发布时间】:2016-07-14 05:38:49
【问题描述】:

在这个页面上提到了这个在 Lisp 中实现类似 C 的 for 循环的宏:https://softwareengineering.stackexchange.com/questions/124930/how-useful-are-lisp-macros

(defmacro for-loop [[sym init check change :as params] & steps]
  `(loop [~sym ~init value# nil]
     (if ~check
       (let [new-value# (do ~@steps)]
         (recur ~change new-value#))
       value#)))

所以可以在代码中使用以下内容:

(for-loop [i 0 , (< i 10) , (inc i)] 
   (println i))

如何将此宏转换为在 Racket 语言中使用?

我正在尝试以下代码:

(define-syntax (for-loop) (syntax-rules (parameterize ((sym) (init) (check) (change)) & steps)
  `(loop [~sym ~init value# nil]
     (if ~check
       (let [new-value# (do ~@steps)]
         (recur ~change new-value#))
       value#))))

但它给出了“错误的语法”错误。

【问题讨论】:

  • 所以,syntax-rulesdefmacro 有很大不同,Scheme/Racket 与 Clojure 也有很大不同。如果您愿意,您也可以使用宏在 Racket 中实现类似的概念,但看起来会有些不同——Clojure 和 Racket 是相关的语言,但它们并不相同,而且它们肯定不兼容源代码。然而,Racket 已经带有一些非常好的 for 循环,它们本身就是普通的旧宏。 Take a look at the docs for more info!
  • 有很多有用的,但不是经典的。我们如何在 Racket 中为此编写宏。
  • 我已经在回答中提到了如何在 Racket 中实现这一点,但同样值得注意的是,Racket do loop 与您在问题中提到的 for-loop 宏几乎相同。但是,我在回答中详细说明了为什么几乎从不使用这种结构。

标签: macros racket


【解决方案1】:

您在问题中包含的代码 sn-p 是用 Clojure 编写的,它是 Lisp 的众多方言之一。另一方面,Racket 是 Scheme 的后裔,它是与 Clojure 完全不同的语言!两者都有宏,是的,但是两种语言的语法会有些不同。

Racket 宏系统相当强大,但syntax-rules 实际上是一种稍微简单的宏定义方法。幸运的是,对于这个宏,syntax-rules 就足够了。 Clojure 宏到 Racket 的或多或少的直接翻译如下所示:

(define-syntax-rule (for-loop [sym init check change] steps ...)
  (let loop ([sym init]
             [value #f])
    (if check
        (let ([new-value (let () steps ...)])
          (loop change new-value))
        value)))

随后可以像这样调用它:

(for-loop [i 0 (< i 10) (add1 i)]
  (println i))

Clojure 代码有许多变化:

  1. Clojure 示例使用`~(分别读作“quasiquote”和“unquote”)将值“插入”到模板中。 syntax-rules 表单会自动执行此替换,因此无需显式执行引用。

  2. Clojure 示例使用以哈希结尾的名称(value#new-value#)来防止名称冲突,但 Racket 的宏系统是卫生的,因此这种转义是完全没有必要——绑定在宏中的标识符默认自动存在于它们自己的范围内。

  3. Clojure 代码使用looprecur,但Racket 支持尾递归,所以翻译只使用“named let,这实际上只是一个非常简单的糖,用于调用自身的立即调用的lambda。

  4. 1234563 #f 而不是 nil 表示没有值。
  5. 最后,for-loop 宏的实际使用中不使用逗号,因为, 在 Racket 中的含义有所不同。在 Clojure 中,它被视为空格,因此在那里它也是完全可选的,但在 Racket 中,这将是一个语法错误。

不过,完整的宏教程远远超出了 Stack Overflow 帖子的范围,因此,如果您有兴趣了解更多信息,请查看 the Macros section of the Racket guide

同样值得注意的是,普通程序员不需要自己实现这种宏,因为Racket already provides a set of very robust for loops and comprehensions built into the language。但事实上,它们只是被定义为宏本身——并没有因为它们是内置的而有什么特别的魔力。

Racket 的 for 循环看起来不像传统的 C 风格的 for 循环,因为 C 风格的 for 循环是非常必要的。另一方面,Scheme 以及 Racket 倾向于支持 functional 风格,这种风格避免了变异,并且通常看起来更具声明性。因此,Racket 的循环试图描述更高级别的迭代模式,例如遍历一系列数字或遍历列表,而不是描述如何更新值等低级语义。当然,如果您真的想要类似的东西,Racket provides the do loop,它几乎与上面定义的 for-loop 宏相同,尽管有一些细微差别。

【讨论】:

  • 我一直把do翻译成begin。为什么更喜欢let?好奇。
  • @ChrisJester-Young 因为let 允许内部定义,而begin 不允许。有时begin 更可取,因为它如何拼接到周围的上下文中(它不会引入新的范围),但在这种情况下,没有上下文可以拼接,所以let 始终是首选。
  • 啊,谢谢你的解释!允许内部定义肯定会让它更加用户友好。
【解决方案2】:

我想稍微扩展一下 Alexis 的出色回答。这是一个示例用法,说明了她所说的do 与您的for-loop 几乎相同的含义:

(do ([i 0 (add1 i)])
    ((>= i 10) i)
  (println i))

这个do 表达式实际上扩展为以下代码:

(let loop ([i 0])
  (if (>= i 10)
      i
      (let ()
        (println i)
        (loop (add1 i)))))

以上版本使用了一个名为let,这被认为是在Scheme中编写循环的常规方式。

Racket 还提供 for 理解,在 Alexis 的回答中也提到过,这也被认为是传统的,下面是它的样子:

(for ([i (in-range 10)])
  (println i))

(除了这实际上并没有返回 i 的最终值)。

【讨论】:

    【解决方案3】:

    我想为不熟悉let 的人重写 Alexis 的出色答案和 Chris Jester-Young 的出色答案。

    #lang racket
    (define-syntax-rule (for-loop [var init check change] expr ...)
      (local [(define (loop var value)
                (if check
                    (loop change (begin expr ...))
                    value))]
        (loop init #f)))
    
    (for-loop [i 0 (< i 10) (add1 i)]
              (println i))
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-12-14
      • 1970-01-01
      • 1970-01-01
      • 2020-06-10
      • 2011-07-15
      • 2023-03-22
      • 2019-06-06
      • 1970-01-01
      相关资源
      最近更新 更多