我正在尝试编写一个可以全局和嵌套方式使用的宏,如下所示:
;;; 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
的主体内被调用或者global是在编译时已经可用的信息。
不幸的是,我无法找到解决方案,因为嵌套宏似乎在多个“宏扩展运行”中得到扩展,因此*ctx*
的动态绑定(在with-context
中设置)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
(或其他上下文感知函数/宏)。所以这样的事情也应该是可能的:
(with-context {:foo :bar}
(do-stuff 2)
(some-arbitrary-function)
(do-stuff 3))
(我知道some-arbitrary-function
是关于副作用的,例如它可能会写一些数据库。)
答案 0 :(得分: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-all
。
This answer解释了macroexpand
和clojure.walk/macroexpand-all
之间的差异:基本上,您需要确保所有内部形式都是宏观扩展的。
macroexpand-all
的源代码显示how it is implemented。
因此,您可以按如下方式实现宏:
(defmacro with-context [ctx form]
(binding [*ctx* ctx]
(clojure.walk/macroexpand-all form)))
在这种情况下,动态绑定应该在内部宏内部可见。
答案 1 :(得分:2)
我会保持简单。
这是解决方案避免额外*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}
答案 2 :(得分:1)
使用一些宏魔法还有另外一个变体:
(defmacro with-context [ctx & body]
(let [ctx (eval ctx)]
`(let [~'&ctx ~ctx]
(binding [*ctx* ~ctx]
(do ~@body)))))
在此定义中,我们为let
引入了另一个ctx
绑定。 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)))
在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