我在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
来自哪里?
答案 0 :(得分:36)
宏有两个隐藏的参数&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
获得(-1)arity异常的原因是因为编译器在生成常规宏用法的错误消息时会减去这两个隐藏的参数。第一个版本的ttt
的真实信息是“args(1)的数量错误”,因为你提供了一个参数a
但是自我调用没有提供另外两个隐藏的参数。 / p>
在实践中,我建议完全避免使用多个ardom宏。相反,考虑一个辅助函数来代表宏完成大部分工作。实际上,这通常也是其他宏的好习惯。
(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)
所以,
user=> (macroexpand '(ttt))
(clojure.core/println 1 2 3)
答案 1 :(得分:2)
当Clojure处理ttt
宏的定义时,它尚未创建,不能用于宏定义内的源代码转换。对于编译器,你的宏就像(好吧,不是真的,但它是一个很好的例子):
(defmacro ttt0 [] (ttt1 1))
(defmacro ttt1 [a] (ttt2 a 2))
(defmacro ttt2 [a b] (ttt3 a b 3))
(defmacro ttt3 [a b c] `(println ~a ~b ~c))
尝试评估ttt0
的定义,你会得到:
CompilerException java.lang.RuntimeException:无法在此上下文中解析符号:ttt1
因此,当Clojure处理宏的定义时,它必须在定义的非引用部分中扩展宏,就像代码的任何其他部分一样。它在ttt1
失败,必须在您的情况下失败。我的猜测是它就像一个bug。很难说为什么得到-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
时会执行。