【问题标题】:Implementing until as a macro实现直到作为宏
【发布时间】:2018-10-23 14:19:14
【问题描述】:

这是我失败的尝试:

(defmacro until
  [condition body setup increment]
  `(let [c ~@condition]
    (loop [i setup]
      (when (not c)
        (do
          ~@body
          (recur ~@increment))))))

(def i 1)

(until (> i 5)
  (println "Number " i)
  0
  (inc i))

我得到:CompilerException java.lang.RuntimeException:不能让限定名称:clojure-noob.core/c

我期待这个输出: Number 1 Number 2 Number 3 Number 4 Number 5

怎么了?

【问题讨论】:

    标签: clojure macros


    【解决方案1】:

    宏存在一些问题:

    • 您需要为宏内部的绑定生成符号。一种方便的方法是使用# 为名称添加后缀。否则,宏中的绑定可能会掩盖代码中其他地方的绑定。
    • 某些宏输入在未引用时被不必要地拼接,即~@ 而不是~

    这是一个可以编译/扩展的宏版本:

    (defmacro until [condition body setup increment]
      `(let [c# ~condition]
         (loop [i# ~setup]
           (when-not c#
             ~body
             (recur ~increment)))))
    

    但这将在您的示例中永远循环,因为condition 只评估一次,i 的值无论如何都不会改变。我们可以解决这个问题:

    (defmacro until [condition body increment]
      `(loop []
         (when-not ~condition
           ~body
           ~increment
           (recur))))
    

    如果我们想改变它的值,我们需要让i 可变:

    (def i (atom 1))
    
    (until (> @i 5)
      (println "Number " @i)
      (swap! i inc))
    ;; Number  1
    ;; Number  2
    ;; Number  3
    ;; Number  4
    ;; Number  5
    

    但现在until 开始看起来很像while 的补充,而且它的额外复杂性似乎没有什么好处。

    (defmacro until [test & body]
      `(loop []
         (when-not ~test
           ~@body
           (recur))))
    

    这个版本的untilwhile 相同,只是测试被反转了,上面带有原子的示例代码仍然表现正确。我们可以通过直接使用while 来进一步简化until,它最终会扩展为相同的代码:

    (defmacro until [test & body]
      `(while (not ~test) ~@body))
    

    【讨论】:

    • 感谢您的详细回复。我现在对一个问题感到困惑:为什么您的第一个工作示例在 ~body 处没有 @ 符号而工作,而第二个需要它?
    • @AdamSilver body 在前几个示例中是单个表达式,但在后面的示例中是表达式列表。注意&
    【解决方案2】:

    也更改let 行:

    ... 
    `(let [c# ~@condition]
    ... 
    

    然后将c的所有引用重命名为c#。后缀# 生成一个唯一的、非命名空间限定的标识符,以确保宏创建的符号不会与宏扩展到的上下文中的任何现有符号发生冲突。每当您以带引号的形式绑定符号时,您应该使用# 来防止冲突,除非您有充分的理由不使用它。

    为什么在这种情况下需要这样做?我不记得确切的原因,但如果我没记错的话,任何以语法引用形式 (`()) 绑定的符号都是命名空间限定的,您不能使用 let 创建命名空间限定符号。

    您可以通过键入以下内容重新创建错误:

    (let [a/a 1]
      a/a)
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多