【问题标题】:Passing compile-time state between nested macros in Clojure在 Clojure 中的嵌套宏之间传递编译时状态
【发布时间】:2016-10-09 12:18:09
【问题描述】:

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

;;; 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 从运行时转移到编译时,因为无论是从 with-context 的主体内还是全局调用 do-stuff 都是已经存在的信息在编译时可用。

不幸的是,我一直没能找到解决方案,因为嵌套宏似乎在多个“宏扩展运行”中得到扩展,所以*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 是关于副作用的,例如它可能会向数据库写入一些内容。)

【问题讨论】:

    标签: clojure macros nested state


    【解决方案1】:

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

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

    【讨论】:

    • 太棒了,这正是我正在寻找的,谢谢!有趣的事实:我确实用macroexpandmacroexpand-1 尝试过这个,但它没有用。我根本不知道macroexpand-all。非常感谢。但是,我想知道我在宏之间传递这样的状态的想法在概念上是否正确。从代码中调用macroexpand 之类的东西总是让人感觉很尴尬。我是否在这里遗漏了一个能够以更好(更惯用、更简洁)的方式解决我的问题的习语?
    • 对此我没有异议:毕竟,功能是可以使用的。这有点不寻常,因此您必须为with-context*ctx* 以及任何依赖它的宏提供良好的文档。它是一种耦合形式,但如果你需要它,它就可以工作。我不知道有什么比这更“干净”的了。
    【解决方案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}
    

    【讨论】:

    • 谢谢。这真的很简单,功能更强大,你是对的。但是,这仅在线程宏块中使用do-stuff(或其他上下文感知函数/宏)时有效。如果我希望能够在该主体中使用不接受上下文作为最后一个参数的任意函数/宏怎么办?例如。 (->> {:a 3 :b 4} (do-stuff 1) (with-open [r stream] ...))。有任何想法吗? (抱歉,我最初的问题确实没有提到这是一项要求,因为我试图使示例尽可能简单)。
    【解决方案3】:

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

    (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
    

    【讨论】:

    • 谢谢。这是一个非常有趣的解决方案,涉及更多。我只是想知道依赖&env 值的实现是否是个好主意(假设有.init.eval 之类的方法)。但是,这提供了一个有趣的见解,让您了解如何从宏中的&env 中受益。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-10-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多