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

Advertisements
Comments
  1. Nick Parker says:

    Good post, this also reminds me of the anaphoric macros PG discusses in On Lisp.

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 )

Google+ photo

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

Connecting to %s