Dynamic scope in macros

699 views Asked by At

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?

2

There are 2 answers

0
Leonid Beschastny On BEST ANSWER

Macro expansion take place during the compilation of you program, so it's imposisble to predict the future value of dynamic variable at that time.

But, probably, you don't need to evaluate *a* during macro expansion and just wants to leave it as is. It this case *a* will be evaluated when the actual code is called. In this case you should quote it with ` symbol:

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

Your implementation of m-get-a causes clojure to replace (m-get-a) with its value when the code is compiled, which is the core binding of *a*, while my wersion causes it to replace (m-get-a) with variable *a* itself.

1
ymln On

You need to quote *a* to make it work:

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"