This article covers Chapter 7, Macros.
A Few Simple Macros
OL begins with a simple
nil! macro that sets something to nil.
nil! is implemented as a macro in Common Lisp (CL)
nil needs to generate a special form. Clojure puts much more careful boundaries around mutable state, so most Clojure data structures are not set-able at all. The few things that can be set are reference types, each with an explicit API and concurrency semantics.
Because setters go through an explicit API instead of a special form, the Clojure
nil! does not need to be macro at all. Here is a
nil! for Clojure atoms:
(defn nil! [at] (swap! at (fn [_] nil)))
swap! function is specific to atoms. Usage for
nil! looks like:
(def a (atom 10)) (nil! a) @a -> nil
The next interesting macro in OL is
nif, which demonstrates the use of backquoting. One way to implement Clojure
((use '[clojure.contrib.fcase :only (case)]) (defmacro nif [expr pos zer neg] `(case (Integer/signum ~expr) -1 ~neg 0 ~zer 1 ~pos))
There are a few interesting differences from CL here:
- Clojure unquoting uses
~@instead of CL's
,@. This allows Clojure to treat commas as whitespace.
- Clojure does not have a built-in
signum, but it has access to all of Java, including
caseis not part of core, and is provided by Clojure Contrib.
Defining Simple Macros
OL demonstrates the "fill in the blanks" approach to writing macros:
- Write the desired expansion.
- Write the desired macro invocation form.
- Use backquoting to create a template based on the desired expansion.
- Use unquoting to substitute forms from the macro invocation into the template.
As examples, OL uses
our-while. The Clojure equivalents are:
(defmacro our-when [test & body] `(if ~test (do ~@body)))
(defmacro our-while [test & body] `(loop  (when ~test ~@body (recur))))
There is one interesting new thing here. Clojure'
recur is an explicit way to denote a self-tail-call so that Clojure can implement it with a non-stack-consuming iteration. (Clojure cannot optimize tail calls in a generic way due to limitations of the JVM.)
It is also worth noting that
while loops are uncommon in Clojure. They rely on side effects that change the result of
test, and most Clojure functions avoid side effects.
Destructuring in Macros
Both Clojure and CL support destructuring in macro definitions. The OL example of this is a
when-bind macro. Here is a literal translation in Clojure:
(defmacro when-bind [bindings & body] (let [[form tst] bindings] `(let [~form ~tst] (when ~form ~@body))))
[form tst] is a destructuring bind. The first element of
bindings binds to
form, and the second element to
tst. Usage looks like this:
(when-bind [a (+ 1 2)] (println "a is" a)) a is 3
Do not use the
when-bind as defined above. Clojure provides a better version called
; from Clojure core (defmacro when-let [bindings & body] (if (vector? bindings) (let [[form tst] bindings] `(let [temp# ~tst] (when temp# (let [~form temp#] ~@body)))) (throw (IllegalArgumentException. "when-let now requires a vector for its binding"))))
when-let adds two features not present in
when-letrequires that the binding form be a vector. This leads to the "arguments in square brackets" style that distinguishes Clojure from many Lisps.
when-letintroduces a temporary binding
temp#using Clojure's auto-gensym feature.
The temporary binding of
temp# keeps the binding form from being expanded directly into the
when, because some binding forms are not legal for evaluation. The following output shows the difference:
(when-bind [[a & b] [1 2 3]] (println "b is" b)) ->java.lang.Exception: Unable to resolve symbol: & in this context (when-let [[a & b] [1 2 3]] (println "b is" b)) -> b is (2 3)
If it is not clear to you why
when-bind doesn't work, try calling
macroexpand-1 on both the forms above.
The concepts in OL Chapter 7 translate fairly directly from Common Lisp into Clojure. The bigger differences are choices of idiom. Many of the examples in Common Lisp presume mutable state. In the typical Clojure program these forms would be in the minority.
- The sample code is available at http://github.com/stuarthalloway/onlisp-clojure.
- Thanks to Chouser for setting me straight on why
- 2008/12/12: initial version