Evolving Clojure Through Macros

Clojure

2026-08-20 10:30

Where we are

A claim from unit 1

your entire Clojure program is a series of lists: the very source code of your program is interpreted by the Clojure compiler as lists

Unit 2 added: cond is a nested list of pairs, and generating a list is easy.

This unit collects on both

Everything so far has been about using a language.

This is about changing it.

The route

  1. The phases — and where a macro acts
  2. Why a function cannot do it — the unless example
  3. Templates — syntax quote, unquote, splicing, gensym
  4. Clojure’s own macros
  5. Your own — five of them
  6. Judgement — when, and what it costs

The phases

Four of them

The phases of the Clojure runtime — the separation that makes macros possible.

What each does

Read — text becomes data structures. Reader macros act here.

Macroexpand — macros are called with unevaluated forms and return forms.

Compile — the result becomes bytecode.

Run — the bytecode executes.

Textual substitution

(def a-ref (ref 0))

(dosync
  (ref-set a-ref 1))

You would rather write:

(sync-set a-ref 1)
(defmacro sync-set [r v]
  (list 'dosync
        (list 'ref-set r v)))

Look at what that body does

It is an ordinary function that builds a list.

The first element is the quoted symbol dosync.

That list is the code that will run.

Two consequences

Arguments are forms, not values.

(sync-set a-ref 1) hands the macro the symbol a-ref, not the ref.

Macros have no runtime existence.

They cannot be passed to map, stored, or called dynamically. They are not values.

Why unless cannot be a function

The goal

(defn exhibits-oddity? [x]
  (unless (even? x)
    (println "Very odd, indeed!")))

The attempt

(defn unless [test then]
  (if (not test)
    then))
(exhibits-oddity? 11)
Very odd, indeed!
;=> nil

Correct.

Now an even number

(exhibits-oddity? 10)
Very odd, indeed!
;=> nil

It printed anyway.

Why

Clojure evaluates arguments before calling.

By the time unless receives then, the println has already run.

The function only sees the nil it left behind.

And the failure is quiet

Invisible when the body is pure.

Visible only when the body has a side effect or is expensive —

which is exactly when you care.

The workaround

(defn unless [test then-thunk]
  (if (not test)
    (then-thunk)))

(defn exhibits-oddity? [x]
  (unless (even? x)
    #(println "Rather odd!")))

Works — and every caller must remember the #().

Forget it once and the bug is back, silently.

The macro

(defmacro unless [test then]
  (list 'if (list 'not test)
        then))
(macroexpand '(unless (even? x) (println "Very odd, indeed!")))
;=> (if (not (even? x)) (println "Very odd, indeed!"))

The body was never evaluated. It was moved.

The three expansion tools

  • macroexpand-1 — one step
  • macroexpand — until the outermost form is not a macro
  • macroexpand-all — everything, including nested

None of them run the result.

The general rule

A function receives values. A macro receives forms.

Must it decide whether to evaluate? Not a function.

Must it decide when, or how many times? Not a function.

Otherwise — use a function.

Templates and hygiene

Syntax quote

(defmacro unless [test then]
  `(if (not ~test)
     ~then))

Two differences from ':

  • symbols resolve to their namespaces
  • the form becomes a template

Forget the unquote

(defmacro unless [test then]
  `(if (not ~test)
     then))
(macroexpand '(unless (even? x) (println "Very odd, indeed!")))
;=> (if (clojure.core/not (even? x)) user/then)

user/then — the macro’s own parameter name, inserted literally.

Several body expressions

(defmacro unless [test & exprs]
  `(if (not ~test)
     (do ~exprs)))
(exhibits-oddity? 11)
Odd!
Very odd!
NullPointerException

Printed both, then threw.

The expansion says why

(macroexpand-1 '(unless (even? x)
                  (println "Odd!")
                  (println "Very odd!")))
;=> (if (clojure.core/not (even? x))
      (do ((println "Odd!") (println "Very odd!"))))

Double parentheses. Both printlns evaluate to nil, giving (nil nil)

an attempt to call nil as a function.

The fix

(defmacro unless [test & exprs]
  `(if (not ~test)
     (do ~@exprs)))

~ inserts the sequence. ~@ inserts its elements.

Variable capture

(defmacro def-logged-fn [fn-name args & body]
  `(defn ~fn-name ~args
     (let [now (System/currentTimeMillis)]
       (println "[" now "] Call to" (str (var ~fn-name)))
       ~@body)))
(def-logged-fn printname [name] (println "hi" name))
CompilerException Can't let qualified name: user/now

The error is protecting you

Suppose now were not qualified:

(def-logged-fn daily-report [the-day] ...)

(let [now "2009-10-22"]
  (daily-report now))

daily-report would see a number, not "2009-10-22".

The macro’s let captured the caller’s now.

Auto-gensym

(let [now# (System/currentTimeMillis)]
  ...)

now# might expand to now_14187_auto_.

Every occurrence in the template gets the same generated symbol.

Division of labour

Syntax quote’s namespace resolution → prevents capture of function names

Auto-gensym → prevents capture of local bindings

Clojure’s own macros

comment

(defmacro comment [& body])

The whole implementation. Returns nil.

Proof that arguments really are unevaluated — the body can be nonsense.

declare

(defmacro declare [& names]
  `(do
     ~@(map #(list 'def %) names)))
(macroexpand '(declare add multiply subtract divide))
;=> (do (def add) (def multiply) (def subtract) (def divide))

Several forms from a variable number of arguments — that is ~@.

defonce

(defmacro defonce [name expr]
  `(let [v# (def ~name)]
     (when-not (.hasRoot v#)
       (def ~name ~expr))))

Cannot be a function: it must not evaluate expr when the name already exists.

and — the one worth reading

(defmacro and
  ([] true)
  ([x] x)
  ([x & next]
   `(let [and# ~x]
      (if and# (and ~@next) and#))))

Three things at once

(macroexpand '(and (even? x) (> x 50) (< x 500)))
;=> (let* [and_4357_auto_ (even? x)]
      (if and_4357_auto_
        (clojure.core/and (> x 50) (< x 500))
        and_4357_auto_))

Short-circuits · recursive · gensym so x is evaluated once

And it explains why and returns the deciding value, not a boolean.

time

(defmacro time [expr]
  `(let [start# (. System (nanoTime))
         ret# ~expr]
     (prn (str "Elapsed time: "
               (/ (double (- (. System (nanoTime)) start#)) 1000000.0)
               " msecs"))
     ret#))

Must be a macro: it needs the form unevaluated in order to time evaluating it.

What the set teaches

Every technique from the previous section, in production code.

Each macro exists for a reason expressible in one sentence.

None of them is a macro for style.

Writing your own

infix

(defmacro infix [expr]
  (let [[left op right] expr]
    (list op left right)))

No evaluation control. No bindings. Purely rearranging.

A macro is, at bottom, a function from a list to a list.

randomly

(defmacro randomly [& exprs]
  (let [len (count exprs)
        conditions (map #(list '= % '(rand-int len)) (range len))]
    `(cond ~@(interleave conditions exprs))))
(randomly (println "amit") (println "deepthi") (println "adi"))

Must be a macro — a function would evaluate all three.

defwebmethod: the problem

(defn login-user [request]
  (let [username (:username request)
        password (:password request)]
    (if (check-credentials username password)
      (str "Welcome back, " username "!")
      (str "Login failed!"))))

That destructuring preamble repeats in every handler.

defwebmethod: the macro

(defmacro defwebmethod [name args & exprs]
  `(defn ~name [{:keys ~args}]
     ~@exprs))
(defwebmethod login-user [username password]
  (if (check-credentials username password)
    (str "Welcome, " username "!")
    (str "Login failed!")))

The shape behind every def-something in every library you will use.

defnn — keyword arguments

(print-details :start-date "10/22/2009" :name "Rob" :salary 1000000)
Name: Rob
Salary: 1000000
Started on: 10/22/2009
(defmacro defnn [fname [& names] & body]
  (let [ks {:keys (vec names)}]
    `(defn ~fname [& {:as arg-map#}]
       (let [~ks arg-map#]
         ~@body))))

The macro computes part of the code, not merely templates it.

assert-true

(assert-true (>= (* 2 4) (/ 18 2)))
;=> RuntimeException (* 2 4) is not >= 9

The message quotes the source you wrote.

Only a macro can do this

(defmacro assert-true [test-expr]
  (let [[operator lhs rhs] test-expr]
    ...))

A function receives only false, and can say nothing about where it came from.

Every testing library depends on this trick.

When not to write a macro

The rule

If a function can do it, use a function.

Macros are warranted when

  • you must control whether or when something is evaluated
  • you must introduce a binding the caller can see
  • you must define a name
  • you need the source form, not its value

What a macro costs

  • not a value — cannot be passed to map, stored, or composed
  • runs at a different time — errors surface as confusing expansions
  • harder for a reader — they must know your macro first

The honest summary

Used well, a macro removes a repeated shape no function could abstract.

Used badly, it makes ordinary code unreadable for no gain.

Summary

The six things to carry away

  • A macro runs between reading and compiling, on forms rather than values.
  • Homoiconicity makes macros ordinary — there is no separate macro language.
  • unless cannot be a function because arguments evaluate first.
  • Templates make macros writable; expansion makes them debuggable.
  • Variable capture is silent, and auto-gensym prevents it.
  • The judgement matters more than the mechanism.

Where next

More on Functional Programming sets macros aside.

Higher-order functions and closures, pushed until they become an object system.

And delayed evaluation from the other side — what a closure can do that a macro cannot.