Skip to content
AgileLogics
8 min read

Macros

Prerequisites: Reading Clojure code, Keywords and symbols

Because Clojure code is made of Clojure data, a function can build code. A macro is such a function that runs at compile time: it receives its arguments as unevaluated code and returns new code, which Clojure then compiles in its place. when, cond, ->, defn, and for are all macros.

A first macro

(defmacro unless [test & body]
  (list 'if test nil (cons 'do body)))

(unless (> 1 2)
  (println "1 is not greater than 2")
  (println "so this runs"))

(prn (macroexpand-1 '(unless false (println "hi"))))
1 is not greater than 2
so this runs
(if false nil (do (println "hi")))

A function couldn't do this: its arguments would be evaluated before the call, so both printlns would run regardless of the test. The macro receives the code itself and decides where it goes. macroexpand-1 shows what a macro call turns into — your main debugging tool.

Syntax quote

Building code with list and cons gets tedious. Syntax quote (backtick) writes a template:

(defmacro unless [test & body]
  `(if ~test nil (do ~@body)))

(unless false (println "templated macro works"))
(prn (macroexpand-1 '(unless false (println "a") (println "b"))))
templated macro works
(if false nil (do (println "a") (println "b")))
  • ` quotes a template and qualifies symbols with their namespace (a println written inside the template becomes clojure.core/println), so the expansion means the same thing wherever the macro is used. Special forms like if and do stay as they are, and the caller's code (~@body) is inserted unchanged.
  • ~x (unquote) inserts the value of x.
  • ~@xs (unquote-splicing) inserts the items of a list.

Avoiding name clashes with gensyms

(defmacro with-timing [label & body]
  `(let [start# (System/nanoTime)
         result# (do ~@body)]
     (println ~label "took" (if (< (- (System/nanoTime) start#) 1e9) "under a second" "a while"))
     result#))

(def start "user's own start")
(println (with-timing "sum" (reduce + (range 1000))))
(println start)
sum took under a second
499500
user's own start

start# generates a unique symbol (like start__1234__auto__), so the macro's local names can't collide with the caller's — the user's start is untouched. This is called hygiene.

A useful macro: a tiny test DSL

(defmacro check [& pairs]
  `(do
     ~@(for [[expr expected] (partition 2 pairs)]
         `(let [actual# ~expr]
            (println (if (= actual# ~expected) "ok  " "FAIL")
                     (pr-str '~expr) "=>" (pr-str actual#))))))

(check
  (+ 1 2) 3
  (str "a" "b") "ab"
  (count [1 2]) 3)
ok   (+ 1 2) => 3
ok   (str "a" "b") => "ab"
FAIL (count [1 2]) => 2

'~expr inserts the code of each expression as quoted data, so the report shows exactly what was tested — something only a macro can do.

The rules of macros

  1. Don't write a macro when a function will do. Functions compose, can be passed to map, and are easier to test.
  2. Use macros for new control flow (unless, with-retry), binding forms (with-open-style), removing repetitive boilerplate, and compile-time checks or DSLs.
  3. Keep macros thin: have the macro expand into a call to a function that does the real work.
  4. Always check the expansion with macroexpand-1 / macroexpand.