是否有一种干净的方法来实现将"触及"宏调用的动态范围?也许更重要的是,即使有,也应该避免吗?
以下是我在REPL中看到的内容:
user> (def ^:dynamic *a* nil)
> #'user/*a*
user> (defn f-get-a [] *a*)
> #'user/f-get-a
user> (defmacro m-get-a [] *a*)
> #'user/m-get-a
user> (binding [*a* "boop"] (f-get-a))
> "boop"
user> (binding [*a* "boop"] (m-get-a))
> nil
这个m-get-a
宏并不是我真正的目标,它只是我遇到的问题的简化版本。不过,我花了一段时间才意识到,因为我一直在用macroexpand
进行调试,这让一切看起来都很好:
user> (binding [*a* "boop"] (macroexpand '(m-get-a)))
> "boop"
在外部binding
调用上执行macroexpand-all
(从clojure.walk
使用)使我相信"问题"(或功能,视情况而定)是在动态绑定之前对(m-get-a)
进行评估:
user> (macroexpand-all '(binding [*a* "boop"] (f-get-a)))
> (let* []
(clojure.core/push-thread-bindings (clojure.core/hash-map #'*a* "boop"))
(try (f-get-a) (finally (clojure.core/pop-thread-bindings))))
user> (macroexpand-all '(binding [*a* "boop"] (m-get-a)))
> (let* []
(clojure.core/push-thread-bindings (clojure.core/hash-map #'*a* "boop"))
(try nil (finally (clojure.core/pop-thread-bindings))))
以下是我的解决方法:
(defmacro macro-binding
[binding-vec expr]
(let [binding-map (reduce (fn [m [symb value]]
(assoc m (resolve symb) value))
{}
(partition 2 binding-vec))]
(push-thread-bindings binding-map)
(try (macroexpand expr)
(finally (pop-thread-bindings)))))
它将使用相关的动态绑定来评估单个宏表达式。但我不喜欢在宏中使用macroexpand
,这似乎是错误的。在宏中解析符号似乎也是错误的——这感觉就像是一个半途而废的eval
。
最终,我正在为一种名为qgame的"语言"编写一个相对轻量级的解释器,我希望能够在解释器执行内容的上下文之外定义一些动态呈现函数。呈现函数可以对顺序指令调用和中间状态进行一些可视化。我用宏来处理解释器执行的东西。到目前为止,我实际上已经切换到完全不使用宏,而且我有render函数作为执行函数的参数。老实说,不管怎样,那样似乎简单多了。
但我还是很好奇。这是Clojure的一个预期功能吗,即宏不能访问动态绑定?不管怎样,有可能绕过它(不诉诸黑暗魔法)吗?这样做有什么风险?
宏扩展发生在程序的编译过程中,因此不可能预测动态变量的未来值。
但是,在宏扩展过程中,您可能不需要计算*a*
,只想保持原样。在这种情况下,*a*
将在调用实际代码时进行计算。在这种情况下,您应该引用`symbol:
(defmacro m-get-a [] `*a*)
您对m-get-a
的实现导致clojure在编译代码时用其值替换(m-get-a)
,这是*a*
的核心绑定,而我的wersion导致它用变量*a*
本身替换(m-get-a)
。
您需要引用*a*
才能使其工作:
user=> (def ^:dynamic *a* nil)
#'user/*a*
user=> (defmacro m-get-a [] `*a*)
#'user/m-get-a
user=> (binding [*a* "boop"] (m-get-a))
"boop"