Posts Tagged ‘macros’

defmacro! revisited

Posted: September 28, 2011 in Clojure
Tags: ,

In my last post, I’ve introduced the defmacro! macro, which is just like defmacro, except that it guarantees that all of the arguments are evaluated once only.

However, in contrast to Doug Hoyte’s defmacro! he introduced in Let over Lambda, my macro expanded into a normal defmacro form that expanded into a form where all args were evaluated exactly once.

Clearly, this was totally flawed, because in, say, new control structures, you may want to have some argument evaluated never.

So here’s a better version which allows for better control about evaluation. All args with trailing ! (BANG, in Clojure speak) will be evaluated exactly once, and the rest of the args stays under the programmer’s control (note that this version also takes a mandatory docstring):

(defn bang-symbol?
  "Returns true, if sym is a symbol with name ending in a exclamation
  mark (bang)."
  [sym]
  (and (symbol? sym)
       (= (last (name sym)) \!)))

(defmacro defmacro!
  "Defines a macro name with the given docstring, args, and body.
  All args ending in an exclamation mark (!, bang) will be evaluated only once
  in the expansion, even if they are unquoted at several places in body.  This
  is especially important for args whose evaluation has side-effecs or who are
  expensive to evaluate."
  [name docstring args & body]
  (let [bang-syms (filter bang-symbol? args)
        rep-map (apply hash-map
                       (mapcat (fn [s] [s `(quote ~(gensym))])
                               bang-syms))]
    `(defmacro ~name
       ~docstring
       ~args
       `(let ~~(vec (mapcat (fn [[s t]] [t s]) rep-map))
          ~(clojure.walk/postwalk-replace ~rep-map ~@body)))))

Using that, we can now easily implement the numeric if, nif, you can find in On Lisp and Let over Lambda:

(defmacro! nif
  "Numeric if: evals test! (only once) and executes either pos, zero, or neg
  depending on the result."
  [test! pos zero neg]
  `(cond
    (pos? ~test!)  ~pos
    (zero? ~test!) ~zero
    :else          ~neg))

When evaluating (nif 1 (println "pos") (println "zero") (println "neg")), now there’s only “pos” printed. With the previous defmacro! version, “pos”, “zero”, and “neg” were printed.

UPDATE: Stefan Kamphausen noticed that defmacro! doesn’t work as intended if destructuring is done in the argument list. So here’s yet another version that flattens the argument list when collecting the bang-symbols.

(defmacro defmacro!
  "Defines a macro name with the given docstring, args, and body.
  All args ending in an exclamation mark (!, bang) will be evaluated only once
  in the expansion, even if they are unquoted at several places in body.  This
  is especially important for args whose evaluation has side-effecs or who are
  expensive to evaluate."
  [name docstring args & body]
  (let [bang-syms (filter bang-symbol? (flatten args)) ;; <==
        rep-map (apply hash-map
                       (mapcat (fn [s] [s `(quote ~(gensym))])
                               bang-syms))]
    `(defmacro ~name
       ~docstring
       ~args
       `(let ~~(vec (mapcat (fn [[s t]] [t s]) rep-map))
          ~(clojure.walk/postwalk-replace ~rep-map ~@body)))))

Using that, you can define a strange nif variant that wants a vector, where the first entry is a vector containing the test, and the second entry is a vector of the pos, zero, neg entries.

(defmacro! strange-nif
  "Like nif, but with strange destructuring"
  [[[test!] [pos zero neg]]]
  `(cond
    (pos? ~test!)  ~pos
    (zero? ~test!) ~zero
    :else          ~neg))

;; Trying it...
user> (strange-nif [[1] [:pos :zero :neg]])
:pos
user> (macroexpand '(strange-nif [[1] [:pos :zero :neg]]))
(let [G__1974 1]
     (cond (pos? G__1974) :pos (zero? G__1974) :zero :else :neg))

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