Browse Learn Clojure Foundations as a Java Developer

Control Evaluation Order in Clojure Macros

Learn how macro expansion changes argument evaluation compared with Java method calls, and how to avoid repeated side effects in generated Clojure code.

A Java method receives evaluated argument values. A Clojure macro receives unevaluated forms and returns a new form that will be evaluated later. That one difference explains many macro bugs.

If a macro inserts the same caller form twice, that form can run twice. If the form mutates state, logs, reads I/O, allocates, or throws, the bug is visible only after expansion.

Expansion Before Evaluation

    flowchart TB
	    A["Caller writes macro form"] --> B["Macro receives unevaluated forms"]
	    B --> C["Macro returns expanded Clojure code"]
	    C --> D["Expanded code is compiled or evaluated"]
	    D --> E["Runtime side effects happen here"]

The macro controls where the caller’s forms appear in the generated code. The runtime only sees the expansion.

The Repeated Evaluation Bug

This macro looks like a harmless logging helper:

1(defmacro bad-log [expr]
2  `(do
3     (println "value:" ~expr)
4     ~expr))

The argument appears twice in the expansion. If the caller passes a side-effecting expression, it runs twice:

1(def counter (atom 0))
2
3(bad-log (swap! counter inc))
4;; prints: value: 1
5;; returns: 2

That result surprises Java engineers because a Java method argument would be evaluated once before method entry. A macro argument is not a value yet.

Bind Once, Reuse the Value

Bind the expression once in generated code, then use the generated local:

1(defmacro log-once [expr]
2  `(let [value# ~expr]
3     (println "value:" value#)
4     value#))
5
6(log-once (swap! counter inc))
7;; prints: value: 3
8;; returns: 3

The value# symbol is an auto-gensym. Clojure generates a unique local name so the macro’s temporary binding does not collide with caller code.

Java Comparison

Java method call Clojure macro call
Arguments are evaluated before the method body runs. Arguments are passed as forms to the macro.
Reusing a parameter does not rerun the original expression. Reusing ~expr in the expansion reruns the expression.
Runtime debugger sees the method body. Macro review starts by inspecting the expansion.

Evaluation Review Checklist

Check Safer habit
Caller expression appears more than once. Bind it once with a generated local.
Macro controls body execution. Make skipped, repeated, or delayed execution explicit in the name and docs.
Caller form has side effects. Test with an atom or logging expression to expose repeated evaluation.
Expansion is hard to predict. Use macroexpand-1 before trusting the abstraction.

Knowledge Check

### Why can `~expr` cause a repeated-evaluation bug? - [x] Each occurrence inserts the caller's form into another runtime position. - [ ] It converts the expression to Java bytecode immediately. - [ ] It prevents the macro from expanding. - [ ] It forces the expression to be evaluated before macro expansion. > **Explanation:** A macro works with forms. If the same form appears twice in the expansion, runtime evaluation can execute it twice. ### What is the usual fix when a macro needs to reuse an expression value? - [x] Bind the expression once in generated code using a generated local. - [ ] Replace the macro with Java reflection. - [ ] Call `eval` inside the macro. - [ ] Move the expression into a global var. > **Explanation:** A generated `let` binding evaluates the caller form once, then reuses the resulting value. ### How does Java method argument evaluation differ from macro argument handling? - [x] Java methods receive already evaluated values; macros receive unevaluated forms. - [ ] Java methods receive syntax trees; macros receive bytecode. - [ ] Java evaluates method arguments after the method returns. - [ ] There is no difference. > **Explanation:** This is the core migration point. A macro can decide where and whether a caller form is evaluated.
Revised on Saturday, May 23, 2026