On Lisp -> Clojure, Chapter 7

This article is part of a series describing a port of the samples from On Lisp (OL) to Clojure. You will probably want to read the intro first.

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

The 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 nif is:

  ((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 ~ and ~@ instead of CL's , and ,@. 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 Integer/signum.
  • Clojure's case is 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-when and 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' loop/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))))

The [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 when-let:

  ; 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-bind:

  • when-let requires that the binding form be a vector. This leads to the "arguments in square brackets" style that distinguishes Clojure from many Lisps.
  • when-let introduces 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.

Wrapping up

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.


Notes

Revision history

  • 2008/12/12: initial version
Get In Touch