I encountered a strange problem relating to defmacro in Clojure, I have code like
(defmacro ttt
([] (ttt 1))
([a] (ttt a 2))
([a b] (ttt a b 3))
([a b c] `(println ~a ~b ~c)))
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)
after some investigation, I understand I should write
(defmacro ttt
([] `(ttt 1))
([a] `(ttt ~a 2))
([a b] `(ttt ~a ~b 3))
([a b c] `(println ~a ~b ~c)))
but why the first version failed? and args
is too strange to understand, where -1
comes from?
Macros have two hidden arguments &form
and &env
that provide additional information about invocation and bindings that are the cause of the arity exception here. To refer to other arity versions within the same macro, use quasi-quote expansion.
(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
The reason you get the (-1) arity exception is because the compiler subtracts these two hidden arguments when generating the error message for the general macro usage. The true message here for your first version of ttt
would be "Wrong number of args (1)" because you supplied one argument a
but the two additional hidden arguments were not provided by self-invocation.
In practice, I suggest avoiding multi-arity macros altogether. Instead, consider a helper function to do most of the work on behalf of the macro. Indeed, this is often a good practice for other macros as well.
(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
Your second ttt
version works as well due to the recursive nature of macro-expansion
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)
So,
user=> (macroexpand '(ttt))
(clojure.core/println 1 2 3)
When Clojure processes definition of ttt
macro, it isn't created yet and cannot be used for source code transformation inside the macrodefinition. For compiler, your macro is something like (well, not really, but it is a good example):
(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))
Try to evaluate denition of ttt0
, you will get:
CompilerException java.lang.RuntimeException: Unable to resolve symbol: ttt1 in this context
So, when Clojure processes definition of a macro, it must expand macros in unquoted parts of the definition, as for any other part of code. It fails with ttt1
, and must fail in your case. My guess is that it is something like a bug. It is difficult to say why you get -1
, I think it has to do with internal machinery of the language realization.
Here we can see difference between macros and functions: macros work on any input code to transform it immediately, while a function must be called and everything is always defined and ready for it:
user> (defn ttt
([] (ttt 1))
([a] (ttt a 2))
([a b] (ttt a b 3))
([a b c] :works!))
;; => #'user/ttt
user> (ttt)
;; => :works!
Here calls of ttt
are just instructions, they will be executed when ttt
is called.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With