【问题标题】:How to avoid anaphoric macro in Clojure?如何避免 Clojure 中的照应宏?
【发布时间】:2012-04-03 14:18:51
【问题描述】:

举这个(简化的)例子:

(defmacro make [v & body]
  `(let [~'nv ~(some-calc v)]
     ~(map #(if (= % :value) 'nv %) body)))

现在符号nv 是硬编码的。有没有办法以某种方式生成 nv 并且仍然能够在地图功能中使用它?

顺便问一下,这真的是照应宏吗?

【问题讨论】:

  • 不确定是否可以称之为照应:如果是这样,nv 应该引用宏调用表单的某些部分。如果你从宏体中提取出some-calc,那么你将有一个更合适的照应调用:(make (some-calc x) (do-something-to nv)),其中nv 指的是(some-calc x)。在您的情况下,您将拥有(make x (do-some-thing-to nv)),但随后nv 不会直接引用宏调用表单中的任何内容。
  • @skuro:我明白了,所以基本上这是两全其美的 - 引入一个新符号,但它会一直隐藏,直到有人不小心重新定义它。 Hf 跟踪该错误 :) 幸运的是,根据 amalloy 的建议,明确的 gensym 解决了这个问题。

标签: macros clojure


【解决方案1】:

在您的示例中,some-calcmap 都发生在宏扩展期间,而不是在运行时,因此 nv 无论如何都不需要是 let。宏本身没有正确编写,与符号捕获无关。

【讨论】:

  • 正如我所说,这是一个简化的例子;该宏实际上返回了一个fn,它会被相对频繁地调用,因此是let中的“缓存”。
  • 我真的不买这个,亚历克斯 - 很明显,他的 map 打算在宏扩展期间发生,而且 some-calc 也可能是完全合理的。例如,(some-calc 'x) 可能返回 '(inc (doto x prn)) - 非常适合在该上下文中使用,并且需要为 let 以避免重复打印的副作用。
  • 当然在扩展期间调用mapsome-calc 可能是正确的。我的观点是nvlet 实际上并没有做任何事情,因此宏本身对于nv 的问题没有任何意义。
【解决方案2】:

据我所知,它实际上并不是一个照应宏。

一个照应等价物会给你这样的语法:

(make foo 1 2 3 4 it 6 7 it 8 9)

即已定义符号 it,以便可以在 make 宏的主体内使用它。

我不确定这是否是你想要的,因为我没有足够的上下文来说明如何使用这个宏,但你可以像这样实现上面的:

(defmacro make [v & body]
   `(let [~'it ~v]
      (list ~@body)))

(make (* 10 10) 1 2 3 4 it 6 7 it 8 9)
=> (1 2 3 4 100 6 7 100 8 9)

或者,如果您不是真的想创建新语法,而只是想在某个集合中替换 :value,那么您实际上并不需要宏:最好只使用 replace:

(replace {:value (* 10 10)} [1 2 :value 3 4])
=> [1 2 100 3 4]

【讨论】:

  • 尽管我接受了 amalloy 的回答,但这还是很有帮助的,谢谢!
【解决方案3】:

答案包含在问题中:只需使用 gensym,就像 Clojure 没有自动生成符号时一样。

(defmacro make [v & body]
  (let [value-sym (gensym)]
    `(let [~value-sym ~(some-calc v)]
       ~@(replace {:value value-sym} body))))

请注意,我不确定您是否真的想要~~@ - 这取决于body 是否应该是在let 中执行的表达式序列,还是参数序列到单个函数调用。但是~@ 会更直观/正常,所以我猜是这样。

这个宏是否是照应有点值得怀疑:肯定将nv 引入调用范围是,但它基本上是无意的,所以我会说不。在我的修订版中,我们不再引入nv 或类似的东西,但我们“神奇地”用v 替换:value。不过,我们只在主体的最顶层这样做,所以这不像是引入一个真正的作用域——我会说这更像是让客户端的代码在极端情况下意外中断。

以这种欺骗性行为如何出现的示例为例,假设body 的元素之一是(inc :value)。它不会被宏替换,并且会扩展为(inc :value),这永远不会成功。因此,我建议使用 real 照应宏,它为符号引入了真实范围。类似的东西

(defmacro make [v & body]
  `(let [~'the-value ~(some-calc v)]
     ~@body))

然后调用者可以在他们的代码中使用the-value,它的行为就像一个真实的、常规的本地绑定:你的宏通过魔法引入了它,但它没有任何其他特殊技巧。

【讨论】:

    【解决方案4】:

    一种方法是使用动态绑定。

    (declare ^:dynamic *nv*)
    
    (defmacro make [v & body]
      `(binding [*nv* ~(some-calc v)]
         ~(map #(if (= % :value) *nv* %) body)))
    

    在实践中,动态变量的作用域太宽时会很糟糕(测试和调试程序等更困难),但在这种情况下,如果作用域仅限于需要照应的本地调用上下文,它们可以非常方便。

    这种用法的一个有趣方面是,它与使用宏来隐藏动态绑定的常见习惯用法(很多是 with-* 样式)相反。在这个习语中(据我所知,这并不常见)绑定用于暴露宏隐藏的内容。

    【讨论】:

    • 是的,我最近开始(ab)使用动态绑定,并且仍在探索它们可能有用的案例。感谢您的想法!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-07
    • 1970-01-01
    • 2020-07-29
    • 2012-07-21
    相关资源
    最近更新 更多