2

Is there a clean way of implementing dynamic scope that will "reach" into macro calls? Perhaps more importantly, even if there is, should it be avoided?

Here's what I'm seeing in a 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

This m-get-a macro isn't my actual goal, it's just a boiled down version of the problem I have been running into. It took me a while to realize, though, because I kept debugging with macroexpand, which makes everything seem fine:

user> (binding [*a* "boop"] (macroexpand '(m-get-a)))
> "boop"

Doing macroexpand-all (used from clojure.walk) on the outer binding call leads me to believe that the "issue" (or feature, as the case may be) is that (m-get-a) is getting evaluated before the dynamic binding takes:

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))))

Here's my crack at a workaround:

(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)))))

It will evaluate a single macro expression with the relevant dynamic bindings. But I don't like using macroexpand in a macro, that just seems wrong. It also seems wrong to resolve symbols in a macro--it feels like a half-assed eval.

Ultimately, I'm writing a relatively lightweight interpreter for a "language" called qgame, and I'd like the ability to define some dynamic rendering function outside of the context of the interpreter execution stuff. The rendering function can perform some visualization of sequential instruction calls and intermediate states. I was using macros to handle the interpreter execution stuff. As of now, I've actually switched to using no macros at all, and also I have the renderer function as an argument to my execution function. It honestly seems way simpler that way, anyways.

But I'm still curious. Is this an intended feature of Clojure, that macros don't have access to dynamic bindings? Is it possible to work around it anyways (without resorting to dark magic)? What are the risks of doing so?

4

2 回答 2

6

宏扩展发生在程序编译过程中,因此无法预测当时动态变量的未来值。

但是,您可能不需要*a*在宏扩展期间进行评估,而只想保持原样。这种情况*a*将在调用实际代码时进行评估。在这种情况下,您应该用 ` 符号引用它:

(defmacro m-get-a [] `*a*)

您的实现m-get-a导致 clojure 在(m-get-a)编译代码时替换为其值,这是 的核心绑定*a*,而我的 wersion 导致它替换(m-get-a)为变量*a*本身。

于 2013-12-05T08:45:49.810 回答
5

您需要引用*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"
于 2013-12-05T08:44:42.913 回答