Clojure 如何创建可配置宏

Clojure 如何创建可配置宏,clojure,macros,Clojure,Macros,假设我们要编写一个宏,其行为取决于某种配置。具体来说,在我的例子中,配置只是一个布尔值,指示输出代码中的一些优化。更具体地说,转换如下所示(表示通过==>进行宏扩展): (transform…==>(transform'-opt…==>(transform'…)=>。。。 当进行优化时,否则将忽略transform'-opt,并使用transform'代替它 在配置中使用参数会导致太多重复,因此最好使用带-…样式的本地/配置宏(也可能使用全局样式)。我的意思是,使用类似(带有optvalue

假设我们要编写一个宏,其行为取决于某种配置。具体来说,在我的例子中,配置只是一个布尔值,指示输出代码中的一些优化。更具体地说,转换如下所示(表示通过
==>
进行宏扩展):

(transform…==>(transform'-opt…==>(transform'…)=>。。。
当进行优化时,否则将忽略
transform'-opt
,并使用
transform'
代替它

在配置中使用参数会导致太多重复,因此最好使用带-…样式的本地/
配置宏(也可能使用全局样式)。我的意思是,使用类似
(带有optvalue expr*
的东西,在每个
expr
的宏扩展期间,值
被用作
转换的配置,除非另一个
带有opt
。到目前为止,我选择的解决方案是以以下方式使用动态var和
binding

(需要“[clojure.tools.analyzer.jvm:参考[macroexpand all]”)
(定义^:动态*opt*true)
(带opt[值和主体]的defmacro)
`(do~@(绑定[*opt*值](mapv宏扩展所有实体)))
(反宏变换[…]
`(~(if*opt*`transform'-opt`transform')…)
然而,在macroexpansion中使用动态变量和
macroexpansall
,在我看来有些不传统。我考虑过的其他(也是非常规的)选项包括使用常规var和带有redefs的
、atom和volatile,以及在闭包中隐藏配置,如下所示:

(让[opt…]
(带opt的defmacro…)
(反宏变换…)
[在这种情况下,避免突变和手动宏扩展的一种假想的通用方法可能是在宏中引入另一个隐式
&config
参数和一种新的特殊形式
(使用宏配置[(宏配置)*]expr*
将为各种宏指定用于此参数的值。]


在这种情况下有没有常见的做法?你的方法是什么?为什么?提前感谢。

宏将表示为数据结构的代码转换为另一个数据结构。我将编写一个宏,该宏使用转换代码,并在每次遇到指定表单时插入该参数作为最后一个参数。您可以有一个带有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
,但宏转换使其使用arg
表单接收来自所有周围
的选项。然后,我们可以决定如何组合所有选项,例如使用
(进入[]cat优化选项)
。或者我们可以做
(最后的优化选项)
,如果我们想用arg将最里面的
隐藏在最外面的

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

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

(with-arg [transform [:unroll]]
  (-> :X1
      (transform)))
将打印以下内容:

:X0 []
:X1 [:unroll]

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

这是Clojure的宏系统很难做到的事情,但如果我们有
macrolet
,这将很容易。我记得在《让过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
变量,使用普通的引号,或者只保留一个级别的深度。

您是否可以添加一个更具体的示例,说明您试图解决的问题以及如何使用它?此外,配置选项是什么?这是一个合理的尝试,但除了您提到的问题之外,它不考虑引用(例如
(带arg[f1](cons(f);(fgh))
)或阴影(
(带arg[f1](…(带arg[f2]…))
)。虽然经过反思,我不确定在这种情况下阴影应该做什么,因为我们不是定义一个新的
f
,而是改进一个现有的。因此,我认为您的实现与任何实现一样好,就阴影而言。@amalloy我不确定OP是否真的希望对引用的表达式执行此操作。也许它甚至不是d关于阴影,请参阅更新后的答案。我不确定您的意思是“甚至不可取”。对于
wi肯定不可取