如何创建可配置的宏

How to create configurable macros

假设我们要编写一个宏,其行为取决于某种配置。具体来说,在我的例子中,配置只是一个布尔值,表示输出代码中的一些优化。更具体地说,转换看起来像这样(用 ==> 表示宏展开):

(transform ...) ==> (transform'-opt ...) ==> (transform' ...) ==> ...

进行优化时,否则将省略 transform'-opt 并使用 transform' 代替它。

使用参数进行配置会导致重复过多,因此最好以本地/with-... 样式(也可能是全局样式)配置宏。我的意思是,在每个 expr value 的宏扩展都用作 transform 的配置,除非它被另一个 with-opt 更改。到目前为止,我选择的解决方案是按以下方式使用动态变量和 binding

(require '[clojure.tools.analyzer.jvm :refer [macroexpand-all]])

(def ^:dynamic *opt* true)

(defmacro with-opt [value & body]
  `(do ~@(binding [*opt* value] (mapv macroexpand-all body))))

(defmacro transform [...]
  `(~(if *opt* `transform'-opt `transform') ...))

但是,在宏扩展中使用动态变量和 macroexpand-all 对我来说有点不合常规。我考虑过的其他(也是非常规的)选项包括使用常规 var 和 with-redefs、原子、volatile 以及将配置隐藏在闭包中,如下所示:

(let [opt ...]
  (defmacro with-opt ...)
  (defmacro transform ...))

[也许在这种情况下避免突变和手动宏扩展的一种想象的通用方法是在宏中引入另一个隐式 &config 参数和一个新的特殊形式 (with-macro-configs [<em><strong>(宏配置)*</strong></em>] <em><strong>expr*</strong></em>) 将指定用于各种宏的此参数的值。]

这种情况有什么常见的做法吗?你的方法是什么,为什么?提前致谢。

宏将表示为数据结构的代码转换为另一种数据结构。我会编写一个宏,使用 postwalk 转换代码,并在每次遇到它时插入指定形式的参数作为最后一个参数。你可以有一个宏 with-arg 来进行转换:

(require '[clojure.walk :refer [postwalk]])

(defmacro with-arg [[sym arg] & body]
  `(do ~@(postwalk #(if (and (seq? %)
                             (= sym (first %)))
                      (concat % [arg])
                      %)
                   body)))

假设我们有一些宏可以进行代码转换并且我们可以将选项传递给它。只是为了演示,这里是用于测试的虚拟宏:

(defmacro transform [x & optimization-options]
  `(println ~x ~(into [] cat optimization-options)))

这是一个使用它的例子:

(do
  (println "No optimizations enabled")
  (transform :X)
  (with-arg [transform [:unroll]]
    (println "With unrolling")
    (transform :Y)
    (with-arg [transform [:inline :O3]]
      (println "With some extra options")
      (transform :Z))))

这将打印

No optimizations enabled
:X []
With unrolling
:Y [:unroll]
With some extra options
:Z [:unroll :inline :O3]

在代码示例中,我们仅使用 单个 参数调用 transform,但宏转换使其接收来自 all[ 的选项=42=]周围的with-arg形成。然后我们可以决定如何组合所有选项,例如使用 (into [] cat optimization-options)。或者我们可以做 (last optimization-options) 如果我们想要最里面的 with-arg 遮挡外面的

此方法的一个问题是它可能无法与其他宏(例如 -> 很好地组合。例如,这段代码:

(with-arg [transform [:unroll]]
  (-> :X0
      transform))

(with-arg [transform [:unroll]]
  (-> :X1
      (transform)))

将打印此:

:X0 []
:X1 [:unroll]

但这在实践中可能不是问题,只要您意识到这一点。

这是用 Clojure 的宏系统很难做到的事情,但如果我们有 macrolet 就会很容易。我记得读过,我认为在 Let Over Lambda 中,通过使用 macrolet(其他 Lisp 方言中确实存在),大多数尝试自己遍历代码的代码遍历宏都会变得更简单、更健壮。这样,编译器就会为您完成所有困难的工作,包括正确处理阴影和引用等。

举个简单的例子,假设你有一些宏需要一个布尔参数,并且你希望在词法上下文中它总是 true。使用 macrolet,你可以这样写:

(defmacro change* [code toggle]
  (if toggle
    `(not ~code)
    code))

(defmacro with-toggle [toggle & body]
  `(macrolet [(~'change [code#]
                `(change* ~code# ~~toggle))]
     ~@body))

(with-toggle true
  (change (= 1 2)))

虽然 Clojure 标准库中不存在 macrolet,但 clojure.tools.macro 中有一个实现 几乎 与内置于编译器会。它不支持一些边缘情况,但对于我曾经想写的每一个合理的宏,它都是正确的。这是它适用于我们的示例案例的证据:

user=> (macroexpand '(with-toggle false (change (= 1 2))))
(do (= 1 2))
user=> (macroexpand '(with-toggle true (change (= 1 2))))
(do (clojure.core/not (= 1 2)))

当然,困难的部分是编写 macrolet 的正文,因为您必须处理两级语法引用。我应该知道我在做什么,但我仍然尝试了三四次才能正确使用这个简单的宏。请记住,每个 ~ 都会转义一个语法引用级别,之后您必须决定下一步要做什么:再次转义,使用 gensym# 变量,使用普通引号,或者只保留一个单层深度。