在 Clojure 中的嵌套宏之间传递编译时状态

Passing compile-time state between nested macros in Clojure

我正在尝试编写一个既可以全局使用又可以嵌套使用的宏,如下所示:

;;; global:
(do-stuff 1)

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

以嵌套方式使用时,do-stuff 应该可以访问由 with-context 设置的 {:foo :bar}

我已经能够像这样实现它了:

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

但是,我一直在尝试将 do-stuff 内的 if 从运行时转移到编译时,因为 do-stuff 是否是从 [= 的主体内调用的16=] 或全局是在编译时已经可用的信息。

不幸的是,我一直没能找到解决方案,因为嵌套宏似乎扩展成多个 "macro expansion runs",所以 *ctx* 的动态绑定(在 [=16 中设置) =]) 在 do-stuff 展开后不再可用。所以这不起作用:

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

有什么想法可以实现吗?

或者我的方法是否完全疯狂并且有一种模式可以将状态从一个宏传递到嵌套宏?

编辑:

with-context 的主体应该能够使用任意表达式,而不仅仅是 do-stuff(或其他上下文感知 functions/macros)。所以这样的事情也应该是可能的:

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

(我知道 some-arbitrary-function 是关于副作用的,例如它可能会向数据库写入一些东西。)

我会保持简单。 这是避免在附加 *ctx* 变量中出现状态的解决方案。我认为这是一种更实用的方法。

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

输出:

:arg1 1 :context {:a 3, :b 4}
:arg1 2 :context {:b 5, :a 4}

当代码被宏展开时,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))))

当您退出宏时,您在执行宏期间建立的任何绑定都不再存在(这发生在 macroexpand-1 内)。当内部宏被扩展时,上下文早已不复存在。

但是,您可以直接调用macroexpand,这种情况下绑定仍然有效。但是请注意,在您的情况下,您可能需要调用 macroexpand-allThis answer 解释了 macroexpandclojure.walk/macroexpand-all 之间的区别:基本上,您需要确保所有内部形式都是宏扩展的。 macroexpand-all 的源代码显示 how it is implemented.

因此,您可以按如下方式实现您的宏:

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

在那种情况下,动态绑定应该从内部宏内部可见。

还有一种变体可以做到这一点,使用一些宏魔法:

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

在此定义中,我们为 ctx 引入了另一个 let 绑定。然后,Clojure 的宏系统会将其放入 &env 变量中,在编译时可由内部宏访问。请注意,我们还保留 bindings 以便内部函数可以使用它。

现在我们需要定义函数以从宏的 &env:

中获取上下文值
(defn env-ctx [env]
  (some-> env ('&ctx) .init .eval))

然后你可以轻松定义do-stuff:

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

回复:

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