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?