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

;;; global:
(do-stuff 1)

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

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

我已经能够像这样实现它:
(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)))

但是,我一直在尝试将if中的do-stuff从运行时转移到编译时,因为是从do-stuff体内调用with-context还是从全局调用*ctx*是在编译时已经可用的信息。

不幸的是,我无法找到解决方案,因为嵌套宏似乎在多个“宏扩展运行”中被扩展,因此当with-context扩展时,就不再可用do-stuff的动态绑定(bind)(如with-context中设置的那样)。因此,这不起作用:
(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的主体应该能够使用任意表达式,而不仅是some-arbitrary-function(或其他上下文感知功能/宏)。所以这样的事情也应该是可能的:
(with-context {:foo :bar}
  (do-stuff 2)
  (some-arbitrary-function)
  (do-stuff 3))

(我知道ojit_code是关于副作用的,例如,它可能会向数据库中写入一些内容。)

最佳答案

当代码进行宏扩展时,Clojure computesfixpoint:

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

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

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

因此,您可以按以下方式实现宏:
(defmacro with-context [ctx form]
  (binding [*ctx* ctx]
    (clojure.walk/macroexpand-all form)))

在这种情况下,动态绑定(bind)应该从内部宏内部可见。

09-26 06:05