Learn how Clojure macro hygiene protects caller code from accidental name collisions, and when to use auto-gensyms, explicit gensyms, or intentional caller bindings.
Macro hygiene means the names introduced by a macro should not accidentally collide with names in the caller’s code. Java developers usually rely on lexical scope and compiler checks. Clojure macro authors must also think about names created in generated code.
The rule is simple: if a name is only for the macro’s internal implementation, make it generated and unique.
This macro builds its expansion manually and introduces ordinary local names:
1(defmacro bad-benchmark [expr]
2 (list 'let ['start '(System/nanoTime)
3 'value expr]
4 (list 'println
5 "elapsed"
6 (list '- '(System/nanoTime) 'start))
7 'value))
The expansion introduces start before the caller expression runs. If the caller expression also uses start, the macro’s local can capture that reference:
1(let [start 10]
2 (bad-benchmark (+ start 5)))
The call site looks as if start means 10, but the generated code binds a new start first. That is the kind of bug that makes macros feel unpredictable.
Use generated symbols for macro-owned locals:
1(defmacro benchmark [expr]
2 `(let [start# (System/nanoTime)
3 value# ~expr]
4 (println "elapsed"
5 (- (System/nanoTime) start#))
6 value#))
Symbols ending in # inside a syntax-quoted form become unique generated symbols. The caller’s start remains the caller’s local; the macro’s start# is private to the expansion.
| Name in expansion | Recommended treatment |
|---|---|
| Internal temporary value. | Use name# or an explicit gensym. |
| Caller-chosen binding name. | Accept a symbol from the caller and document that the macro binds it intentionally. |
| Core function or special form. | Let syntax quote qualify it, such as clojure.core/let. |
| Symbol intended to resolve in caller scope. | Be explicit and rare; this creates an anaphoric macro style that teams must understand. |
Some macros intentionally introduce a name chosen by the caller:
1(with-open [reader (clojure.java.io/reader path)]
2 (.readLine reader))
That is not accidental capture. The caller explicitly supplies reader as a binding name. The danger is introducing hidden names the caller did not choose.
| Review question | Why it matters |
|---|---|
| Does the macro introduce a local that the caller did not name? | It should usually be a gensym. |
| Does the body run inside a binding introduced by the macro? | Caller references may resolve differently than expected. |
| Does the macro intentionally expose a binding name? | Document that call-site contract clearly. |
Can macroexpand-1 show the generated names clearly? |
Review the expansion before trusting the macro. |