Clojure中defmacro的多元性

24

我在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",但我得到的是

and I run with (ttt), it suppose to become (println 1 2 3), and print "1 2 3", but what I got is

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又是从哪里来的呢?

2个回答

46

宏有两个隐藏参数

宏有两个隐藏参数&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真正的消息应该是“参数数量错误(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)

那么,

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

1
那么,文档中描述了这两个隐藏参数吗?它们包含什么信息? - xudifsd
1
对于“宏展开是递归的”这一点进行挑剔:macroexpand通过反复调用macroexpand-1来计算不动点,但是macroexpandmacroexpand-1都没有递归地调用自身(除非你的宏这样做)。参见https://dev59.com/SZvga4cB1Zd3GeqP2Xij。 - coredump
我不理解你所传达的微妙之处,但 macroexpand 确实以递归方式调用自身。也许你是在说 macroexpand 不会进入子表单?这是正确的,但当然编译器会进入子表单,这将根据需要触发新的 macroexpand 调用。 - A. Webb
1
显式传递隐藏参数是可行的,但你不应该这样做,因为它依赖于实现细节。准引用方式是唯一的方法。 - Meloman

1
当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: Unable to resolve symbol: ttt1 in this context

因此,在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被调用时执行。


6
很抱歉,这个答案的要点是错误的。将宏视为在编译时运行的简单函数会很有帮助。它们在大多数情况下都以相同的方式工作,只是在不同的时间运行而已。实际上,你可以有一个递归宏,甚至是多元性的宏,尽管这种情况很少见。这个多元性的例子失败了,因为Clojure为宏插入了两个隐藏参数,在内部调用时没有提供这些参数。 - A. Webb

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接