Macros clojure定义中的多重算术

Macros clojure定义中的多重算术,macros,clojure,Macros,Clojure,我在Clojure中遇到了一个与defmacro相关的奇怪问题,我的代码如下 (defmacro ttt ([] (ttt 1)) ([a] (ttt a 2)) ([a b] (ttt a b 3)) ([a b c] `(println ~a ~b ~c))) 我用(ttt),它应该变成(println 1 2 3),然后打印“1 2 3”,但我得到的是 ArityException Wrong number of args (-1) passed to: t1$ttt c

我在Clojure中遇到了一个与defmacro相关的奇怪问题,我的代码如下

(defmacro ttt
  ([] (ttt 1))
  ([a] (ttt a 2))
  ([a b] (ttt a b 3))
  ([a b c] `(println ~a ~b ~c)))
我用
(ttt)
,它应该变成
(println 1 2 3)
,然后打印“1 2 3”,但我得到的是

ArityException Wrong number of args (-1) passed to: t1$ttt clojure.lang.Compiler.macroexpand1 (Compiler.java:6473)
经过一些调查,我明白我应该写信

(defmacro ttt
  ([] `(ttt 1))
  ([a] `(ttt ~a 2))
  ([a b] `(ttt ~a ~b 3))
  ([a b c] `(println ~a ~b ~c)))

但是为什么第一个版本失败了呢?而且
args
太奇怪了,无法理解
-1
从何而来?

当Clojure处理
ttt
宏的定义时,它还没有创建,不能用于宏定义内部的源代码转换。对于编译器,您的宏类似于(嗯,不是真的,但这是一个很好的示例):

尝试评估
ttt0的否认,您将得到:

CompilerException java.lang.RuntimeException:无法在此上下文中解析符号:ttt1

因此,当Clojure处理宏的定义时,它必须在定义的非引号部分展开宏,就像在代码的任何其他部分展开宏一样。它在ttt1中失败,并且在您的情况下必须失败。我猜这有点像虫子。很难说为什么会得到
-1
,我认为这和语言实现的内部机制有关

在这里,我们可以看到宏和函数之间的区别:宏处理任何输入代码以立即对其进行转换,而必须调用函数,并且始终定义并准备好所有内容:

user> (defn ttt
        ([] (ttt 1))
        ([a] (ttt a 2))
        ([a b] (ttt a b 3))
        ([a b c] :works!))
;; => #'user/ttt
user> (ttt)
;; => :works!
这里对
ttt
的调用只是指令,它们将在调用
ttt
时执行。

宏有两个隐藏参数 宏有两个隐藏参数
&form
&env
,它们提供了有关调用和绑定的附加信息,而这些调用和绑定是导致此处arity异常的原因。要引用同一宏中的其他arity版本,请使用准引号展开

(defmacro baz
  ([] `(baz 1))
  ([a] `(baz ~a 2))
  ([a b] `(baz ~a ~b 3))
  ([a b c] `(println ~a ~b ~c)))

user=> (macroexpand-1 '(baz))
(clojure.core/println 1 2 3)

user=> (baz)
1 2 3
nil
Arity异常消息从计数中减去隐藏参数 出现(-1)arity异常的原因是编译器在为常规宏用法生成错误消息时减去了这两个隐藏参数。对于第一个版本的
ttt
,这里的真正信息是“args(1)的数目错误”,因为您提供了一个参数
a
,但两个额外的隐藏参数不是通过自调用提供的

多算术宏在野外并不常见 在实践中,我建议完全避免使用多算术宏。相反,请考虑帮助函数来代表宏执行大部分工作。事实上,对于其他宏来说,这通常也是一个很好的实践

(defn- bar
  ([] (bar 1))
  ([a] (bar a 2))
  ([a b] (bar a b 3))
  ([a b c] `(println ~a ~b ~c)))


(defmacro foo [& args] (apply bar args))

user=> (macroexpand-1 '(foo))
(clojure.core/println 1 2 3)

user=> (foo)
1 2 3
nil
宏扩展是递归的 由于宏扩展的递归性质,您的第二个
ttt
版本也可以正常工作

user=> (macroexpand-1 '(ttt))
(user/ttt 1)
user=> (macroexpand-1 *1)
(user/ttt 1 2)
user=> (macroexpand-1 *1)
(usr/ttt 1 2 3)
user=> (macroexpand-1 *1)
(clojure.core/println 1 2 3)
所以


恐怕这个答案的要点错了。将宏看作是在编译时运行的简单函数是很有帮助的。在大多数情况下,它们的工作方式相同,只是在不同的时间。实际上,您可能会有一个递归宏,甚至是一个多算术宏,尽管这种宏很少见。这个多重arity示例失败了,因为Clojure为宏插入了两个隐藏参数,而这两个参数在内部调用期间没有提供。什么文档描述了这两个隐藏参数?它们包含什么信息?关于“宏扩展是递归的”的吹毛求疵:
macroexpand
通过反复调用
macroexpand-1
来计算一个不动点,但
macroexpand
macroexpand-1
都不会递归调用它们自己(除非您的宏这样做)。另请参见,我不理解您所传达的微妙之处,但是,
macroexpand
确实以递归方式调用自己。也许您是说,
macroexpand
不会下降到子表单中?这是真的,但编译器当然会这样做,然后根据需要触发对macroexpand的新调用。显式传递隐藏参数是可行的,但您不应该这样做,因为它依赖于实现细节。准报价方式是唯一的方式。()
user=> (macroexpand-1 '(ttt))
(user/ttt 1)
user=> (macroexpand-1 *1)
(user/ttt 1 2)
user=> (macroexpand-1 *1)
(usr/ttt 1 2 3)
user=> (macroexpand-1 *1)
(clojure.core/println 1 2 3)
user=> (macroexpand '(ttt))
(clojure.core/println 1 2 3)