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

时间:2016-10-09 12:18:10

标签: clojure macros nested state

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

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

但是,我一直在努力将ifdo-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是关于副作用的,例如它可能会写一些数据库。)

3 个答案:

答案 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-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)))

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

答案 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