Learn when a Clojure macro is justified for custom control flow, how to wrap a body without repeated evaluation, and why Java-style loop imitation is usually the wrong goal.
Custom control flow is the classic macro use case. The macro receives a body as unevaluated forms, decides where that body goes, and returns ordinary Clojure code.
That does not mean every missing Java keyword deserves a macro. Clojure already has if, when, cond, case, loop, recur, doseq, sequence operations, and higher-order functions. A custom control macro should make evaluation clearer, not recreate Java syntax out of habit.
This macro keeps the caller’s body as normal Clojure code while wrapping it in timing behavior:
1(defmacro with-timing [label & body]
2 `(let [started# (System/nanoTime)]
3 (try
4 ~@body
5 (finally
6 (println ~label "took"
7 (/ (- (System/nanoTime) started#) 1000000.0)
8 "ms")))))
Usage stays readable:
1(with-timing "load users"
2 (load-users db)
3 (refresh-cache!))
A function can receive a thunk, but the call site becomes less natural:
1(with-timing-fn "load users"
2 (fn []
3 (load-users db)
4 (refresh-cache!)))
The macro is justified only if preserving the direct body shape matters enough to accept macro complexity.
| Need | Prefer |
|---|---|
| Transform an already computed value. | Function. |
| Pass behavior explicitly as a value. | Function taking a function. |
| Control whether, when, or how often body forms run. | Macro. |
| Hide ordinary code behind clever syntax. | Do not do it. |
1(macroexpand-1
2 '(with-timing "load users"
3 (load-users db)))
When reviewing the expansion, check that the body appears exactly where intended, temporary locals are generated, and no caller expression is duplicated.
Java developers often reach for a macro to recreate a familiar loop:
1(repeat-until done?
2 (step!))
That can be valid, but it is often less clear than an ordinary Clojure loop or sequence pipeline. Before adding a control macro, ask whether a reader would understand the expansion faster than they would understand the macro name.
| Question | Good answer |
|---|---|
| Does the macro need unevaluated body forms? | Yes, otherwise a function is simpler. |
| Does the body run zero, one, or many times? | The name and docs make that obvious. |
| Are temporary locals generated? | Yes, with # or gensym. |
| Can a teammate inspect the expansion quickly? | Yes, the generated code is small. |