When programming macros, it’s often desired to have its arguments evaluated only once. Let’s have a look at a simple example:
user> (defmacro square [x] `(* ~x ~x)) #'user/square user> (square 5) 25
At a first glance, it seems to work. But see what happens here:
user> (def c (let [a (atom 4)] #(swap! a inc))) #'user/c
This defines a counter function, which will increase it’s count on every call. Now let’s feed that to our macro:
user> (square (c)) 30
Oh, shouldn’t that be 25, because we’re increasing our counter to 5? No, because the macroexpansion is
user> (macroexpand-1 '(square (c))) (clojure.core/* (c) (c))
So our counter is increased twice resulting in (* 5 6)
. The lesson to be learned is that when writing a macro, one should take care that every argument is evaluated once, i.e., if you feel the need to unquote an argument more than once, you have to let-bind its value to some gensym and use that later on. This is a fixed version of the square
macro.
user> (defmacro square [x] `(let [x# ~x] (* x# x#))) #'user/square user> (macroexpand-1 '(square (c))) (clojure.core/let [x__5139__auto__ (c)] (clojure.core/* x__5139__auto__ x__5139__auto__))
As you can see, now our counter is incremented only once, and its new value is bound to a generated variable which is used in the body.
But why not do exactly that by default? Here’s a macro that does that:
(defmacro defmacro! "Defines a macro in which all args are evaled only once." [name args & body] (let [rep-map (apply hash-map (mapcat (fn [s] [s `(quote ~(gensym))]) args))] `(defmacro ~name ~args `(let ~~(vec (mapcat (fn [[s t]] [t s]) rep-map)) ~(clojure.walk/postwalk-replace ~rep-map ~@body)))))
Using this macrowriting macro, we can now safely use our first implementation of square
:
user> (defmacro! square [x] `(* ~x ~x)) #'user/square user> (macroexpand-1 '(square (c))) (clojure.core/let [G__5491 (c)] (clojure.core/* G__5491 G__5491)) user> (square (c)) 49
Great, seems to work.
UPDATE: This version of defmacro!
has one serious design flaw. The completely expanded form will evaluate all arguments exactly once, but often you want to be able to have some argument evaluated never. See this sequel post for an enhanced version.