Once-only evaluation for Clojure macros

Posted: September 23, 2011 in Clojure
Tags: ,

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 5)

At a first glance, it seems to work. But see what happens here:

user> (def c (let [a (atom 4)] #(swap! a inc)))

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))

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> (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))])
    `(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> (macroexpand-1 '(square (c)))
(clojure.core/let [G__5491 (c)] (clojure.core/* G__5491 G__5491))
user> (square (c))

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.

  1. Good stuff! On Lisp discusses a macro called `with-gensyms` which is supposed to work quite similarly as defmacro!. One can implement `with-gensyms` trivially in Clojure like so –

    (defmacro with-gensyms
    [names form]
    `(let ~(vec (mapcat (fn [n] [n `(gensym)]) names))

    This, together with the vanilla defmacro will do the job equally well :-)

  2. Tassilo Horn says:

    `with-gensyms’ is not very similar. It simply introduces some fresh variables that you can use in the macro. You don’t need that in Clojure, because there you have auto-gensyms: x#.

    However, Peter Norvig’s `once-only’ is very similar and does the same job. And Doug Hoyte’s `defmacro!’ is almost identical. One think where our versions differ is that he introduces once-only semantics only for arguments starting with o!, like “o!foo”. First, I did the same introducing once-only semantics only for arguments with a trailing exclamation mark. But since I couldn’t find a use-case where you want only-once-semantics explicitly turned of for some arg, I removed that flexibility again… Well, ok, so my version creates a needless let-binding for arguments that are evaluated only once in the macro body anyway…

  3. Does your functionality work for multiple levels of backquotes? Such as in macros defining macros?

    • Tassilo Horn says:

      Yes, it does. Here’s an example of a (totally contrieved) macro that defines another macro (again with defmacro!).

        (defmacro! square-plus-x [x]
          `(defmacro! ~(symbol (str 'square-plus- x)) [~'n]
             `(+ ~~x (* ~~'n ~~'n))))
        user> (square-plus-x 10)  ;; defines a new square-plus-10 macro
        user> (square-plus-10 2)

      Notice that is this case, one has to quote-and-eval (~’) the parameter of the generated macro in order for it not to be namespace qualified.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s