Learn what macros actually do in Clojure, how they differ from functions, and why metaprogramming is most valuable when syntax or evaluation control truly needs to change.
Macros are one of the reasons Lisp languages remain distinctive. They let you transform code before evaluation and extend the language with new forms. But the most important lesson about macros is not that they are powerful. It is that they should be used sparingly and for the right reasons.
Macro: A construct that receives unevaluated forms, returns a new form, and participates in code transformation before normal evaluation.
That makes macros fundamentally different from functions. Functions work on values after evaluation. Macros work on forms before evaluation.
Most abstraction in Clojure should still be done with functions and data. A macro is justified when you need to change:
If none of those are true, a function is usually the better choice.
That rule matters because macros can make code more powerful, but also harder to debug, reason about, and teach.
Macros are practical in Clojure because code is represented as structured forms. A macro receives those forms and returns a new form.
1(defmacro unless [condition & body]
2 `(if (not ~condition)
3 (do ~@body)))
unless is not just a function with a funny name. It changes how the body is arranged syntactically before evaluation.
That is the core of metaprogramming in Clojure: programs can construct and transform other program forms in a direct, structured way.
Compare a function and a macro that seem superficially similar:
1(defn log-value [x]
2 (println x)
3 x)
That function can only work after its argument is evaluated.
A macro can inspect the form itself:
1(defmacro dbg [expr]
2 `(let [value# ~expr]
3 (println "expr:" '~expr "=>" value#)
4 value#))
Now the macro can show both the expression and the result. A function could not do that without the caller quoting forms manually.
Most macro writing depends on a few key tools:
` to build code templates~ to insert evaluated pieces~@ to splice sequences of formsgensym to avoid accidental name capture1(defmacro with-timing [& body]
2 `(let [start# (System/nanoTime)
3 result# (do ~@body)
4 end# (System/nanoTime)]
5 {:result result#
6 :elapsed-ns (- end# start#)}))
The # suffix in start#, result#, and end# creates unique symbols automatically, which helps prevent collisions with names in the caller’s code.
Clojure macros are not “hygienic” by default in the way some languages mean that term. It is the macro author’s job to avoid accidental capture and unintended interactions.
Good habits include:
macroexpand-1Poor macro style often shows up as invisible name capture, hard-to-follow expansions, or abstractions that could have been plain functions.
Good metaprogramming removes repetitive syntactic burden or creates a better declarative surface for a real problem domain. Bad metaprogramming creates local magic that only its author understands.
Strong macro use cases include:
Weak use cases include:
One of the best habits in Clojure is to inspect macro expansion.
1(macroexpand-1 '(unless ready? (println "not ready")))
If the expansion is understandable, the macro is usually on healthier ground. If the expansion is hard to reason about, the abstraction may be too complicated.
graph TD;
A["Need an abstraction"] --> B{"Can a function or data model express it?"}
B -->|Yes| C["Use a function or plain data"]
B -->|No| D{"Need custom syntax or evaluation control?"}
D -->|Yes| E["Use a macro"]
D -->|No| F["Rethink the design before adding magic"]
The diagram below captures the healthiest macro habit in Clojure: prefer ordinary abstractions first, then use macros when syntax or evaluation truly requires them.