5

I'm trying to write a macro that can be used both in a global and nested way, like so:

;;; global:
(do-stuff 1)

;;; nested, within a "with-context" block:
(with-context {:foo :bar}
  (do-stuff 2)
  (do-stuff 3))

When used in the nested way, do-stuff should have access to {:foo :bar} set by with-context.

I've been able to implement it like this:

(def ^:dynamic *ctx* nil)

(defmacro with-context [ctx & body]
  `(binding [*ctx* ~ctx]
     (do ~@body)))

(defmacro do-stuff [v]
  `(if *ctx*
     (println "within context" *ctx* ":" ~v)
     (println "no context:" ~v)))

However, I've been trying to shift the if within do-stuff from runtime to compile-time, because whether do-stuff is being called from within the body of with-context or globally is an information that's already available at compile-time.

Unfortunately, I've not been able to find a solution, because nested macros seem to get expanded in multiple "macro expansion runs", so the dynamic binding of *ctx* (as set within with-context) is not available anymore when do-stuff gets expanded. So this does not work:

(def ^:dynamic *ctx* nil)

(defmacro with-context [ctx & body]
  (binding [*ctx* ctx]
    `(do ~@body)))

(defmacro do-stuff [v]
  (if *ctx*
    `(println "within context" ~*ctx* ":" ~v)
    `(println "no context:" ~v)))

Any ideas how to accomplish this?

Or is my approach totally insane and there's a pattern for how to pass state in such a way from one macro to a nested one?

EDIT:

The body of with-context should be able to work with arbitrary expressions, not only with do-stuff (or other context aware functions/macros). So something like this should also be possible:

(with-context {:foo :bar}
  (do-stuff 2)
  (some-arbitrary-function)
  (do-stuff 3))

(I'm aware that some-arbitrary-function is about side effects, it might write something to a database for example.)

Oliver
  • 279
  • 2
  • 8

3 Answers3

4

When the code is being macroexpanded, Clojure computes a fixpoint:

(defn macroexpand
  "Repeatedly calls macroexpand-1 on form until it no longer
  represents a macro form, then returns it.  Note neither
  macroexpand-1 nor macroexpand expand macros in subforms."
  {:added "1.0"
   :static true}
  [form]
    (let [ex (macroexpand-1 form)]
      (if (identical? ex form)
        form
        (macroexpand ex))))

Any binding you establish during the execution of a macro is no more in place when you exit your macro (this happens inside macroexpand-1). By the time an inner macro is being expanded, the context is long gone.

But, you can call macroexpand directly, in which case the binding are still effective. Note however that in your case, you probably need to call macroexpand-all. This answer explains the differences between macroexpand and clojure.walk/macroexpand-all: basically, you need to make sure all inner forms are macroexanded. The source code for macroexpand-all shows how it is implemented.

So, you can implement your macro as follows:

(defmacro with-context [ctx form]
  (binding [*ctx* ctx]
    (clojure.walk/macroexpand-all form)))

In that case, the dynamic bindings should be visible from inside the inner macros.

Community
  • 1
  • 1
coredump
  • 37,664
  • 5
  • 43
  • 77
  • Awesome, this is exactly what I was searching for, thank you! Fun fact: I have indeed tried this with `macroexpand` and `macroexpand-1` but it didn't work. I simply wasn't aware of `macroexpand-all`. Thanks a lot. However, I'm wondering whether my idea of passing state like this between macros is conceptually proper. Calling something like `macroexpand` from within code always feels awkward. Am I missing an idiom here that would solve my problem in a better (more idiomatic, cleaner) way? – Oliver Oct 10 '16 at 08:01
  • I have no objection about it: after all, the function is made available so that you can use it. It is a little bit unusual, so you have to provide a good documentation to `with-context`, `*ctx*` as well as any macro that depends on it. It is a form of coupling, but if you need it, it works. I don't know of anything "cleaner" than that. – coredump Oct 10 '16 at 08:10
2

I'd keep it simple. This is solution avoids state in an additional *ctx* variable. I think it is a more functional approach.

(defmacro do-stuff 
  ([arg1 context]
    `(do (prn :arg1 ~arg1 :context ~context))
         {:a 4 :b 5})
  ([arg1]
    `(prn :arg1 ~arg1 :no-context)))

(->> {:a 3 :b 4}
     (do-stuff 1)
     (do-stuff 2))

output:

:arg1 1 :context {:a 3, :b 4}
:arg1 2 :context {:b 5, :a 4}
murphy
  • 524
  • 4
  • 16
  • Thanks. That's really simple and more functional, you're right. However, this only works when using `do-stuff` (or other context-aware functions/macros) within the threading macro block. What if I'd like to be able to use arbitrary functions/macros in that body that don't accept context as a last parameter? E.g. `(->> {:a 3 :b 4} (do-stuff 1) (with-open [r stream] ...))`. Any ideas? (Sorry, my original question indeed has not mentioned this is a requirement, as I was trying to make the examples as simple as possible). – Oliver Oct 10 '16 at 07:25
1

there is one more variant to do this, using some macro magic:

(defmacro with-context [ctx & body]
  (let [ctx (eval ctx)]
    `(let [~'&ctx ~ctx]
       (binding [*ctx* ~ctx]
         (do ~@body)))))

in this definition we introduce another let binding for ctx. Clojure's macro system would then put it into the &env variable, accessible by the inner macros at compile-time. Notice that we also keep bindings so that inner functions could use it.

now we need to define the function to get the context value from macro's &env:

(defn env-ctx [env]
  (some-> env ('&ctx) .init .eval))

and then you can easily define do-stuff:

(defmacro do-stuff [v]
  (if-let [ctx (env-ctx &env)]
    `(println "within context" ~ctx ":" ~v)
    `(println "no context:" ~v)))

in repl:

user> (defn my-fun []
        (println "context in fn is: " *ctx*))
#'user/my-fun

user> (defmacro my-macro []
        `(do-stuff 100))
#'user/my-macro

user> (with-context {:a 10 :b 20}
        (do-stuff 1)
        (my-fun)
        (my-macro)
        (do-stuff 2))
;;within context {:a 10, :b 20} : 1
;;context in fn is:  {:a 10, :b 20}
;;within context {:a 10, :b 20} : 100
;;within context {:a 10, :b 20} : 2
nil

user> (do (do-stuff 1)
          (my-fun)
          (my-macro)
          (do-stuff 2))
;;no context: 1
;;context in fn is:  nil
;;no context: 100
;;no context: 2
nil
leetwinski
  • 17,408
  • 2
  • 18
  • 42
  • Thanks. This is a very interesting solution, somewhat more involved. I'm just wondering if it's a good idea to rely on the implementation of the `&env` values (via assuming there are methods like `.init` and `.eval`). But nevertheless, this gives an interesting insight into how you can benefit from `&env` in macros. – Oliver Oct 12 '16 at 14:30