Browse Learn Clojure Foundations as a Java Developer

Write Hygienic Clojure Macros

Learn how auto-gensyms, explicit gensyms, and qualified symbols prevent macro-generated locals from colliding with caller code.

Macro hygiene means generated names do not accidentally capture or collide with names in the caller’s code. Hygiene is not style polish. It is what keeps a macro from changing behavior when a caller happens to use the same local name.

The Capture Problem

This macro introduces a local named value:

1(defmacro bad-measure
2  [& body]
3  `(let [value (do ~@body)]
4     (println "value:" value)
5     value))

The generated value is a name chosen by the macro author. In real macros, manually chosen generated names can collide with caller locals or become confusing once syntax quote qualifies symbols.

The safer pattern is to generate a unique local:

1(defmacro measure
2  [& body]
3  `(let [value# (do ~@body)]
4     (println "value:" value#)
5     value#))

Inside syntax quote, value# becomes an auto-gensym: a unique symbol for this expansion.

Auto-Gensym vs gensym

Tool Use it when
name# You need a generated local inside one syntax-quoted template.
(gensym "prefix") You need to compute or reuse a generated symbol across multiple templates.
Qualified symbols You need generated code to refer to a stable function or var.

Most practical macros start with auto-gensyms. Use explicit gensym only when one generated name must be shared across separately built forms.

A Hygienic Binding Example

1(defmacro with-timing
2  [label & body]
3  `(let [started# (System/nanoTime)
4         result# (do ~@body)
5         elapsed# (- (System/nanoTime) started#)]
6     (println ~label "took" elapsed# "ns")
7     result#))

The generated locals started#, result#, and elapsed# cannot collide with caller locals. The caller’s label and body are still inserted where intended.

1(let [result "caller local"]
2  (with-timing "work"
3    (str result " done")))

The caller’s result remains separate from the generated result#.

Hygiene Review Checklist

Check Good answer
Does the macro introduce locals? Yes, and each generated local is auto-gensymed or explicitly gensymed.
Are caller forms inserted only where intended? Yes, with ~ or ~@ at controlled points.
Are core functions resolved predictably? Yes, syntax quote qualifies them or the code uses explicit namespaces.
Can the expansion be read without guessing names? Yes, generated locals are boring and limited.

Java Translation

Java-generated-code concern Clojure macro hygiene concern
Generated fields conflict with user fields. Generated locals collide with caller locals.
Generated source imports the wrong class. Generated symbols resolve in the wrong namespace.
Templates use hidden temporary variables. Macros introduce unexplained temporary symbols.

Knowledge Check

### What does auto-gensym syntax such as `result#` do inside syntax quote? - [x] It creates a unique generated local for that macro expansion. - [ ] It dereferences an atom. - [ ] It marks a var as private. - [ ] It forces evaluation at compile time. > **Explanation:** Auto-gensyms prevent generated locals from colliding with caller locals. ### When should explicit `gensym` be considered? - [x] When a generated symbol must be reused across separately built forms. - [ ] Whenever a function could have been used instead. - [ ] To make runtime calls faster. - [ ] To disable namespace qualification. > **Explanation:** Auto-gensym is enough for one syntax-quoted template. Explicit `gensym` helps when code generation is assembled in multiple steps. ### What is the main risk of non-hygienic generated names? - [x] Caller names and macro-generated names can collide or capture each other. - [ ] The JVM cannot load the namespace. - [ ] The macro always expands twice. - [ ] Clojure stops supporting Java interop. > **Explanation:** Name collisions can change generated code behavior in subtle ways, especially when the caller uses a local with the same name.
Revised on Saturday, May 23, 2026