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