Passing compile-time state between nested macros in Clojure

房东的猫 提交于 2019-12-04 09:36:05
coredump

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.

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}

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
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!