Browse Learn Clojure Foundations as a Java Developer

Build a Small Clojure Macro Example

Walk through a small timing macro that shows when macro syntax is useful, how to keep generated code readable, and how Java engineers should review the expansion before using it.

A good first macro should be small enough that the expansion is less mysterious than the macro itself. This page builds a timing macro because it needs a body form, a local binding, and a guaranteed result value.

Do not start macro practice with a DSL. Start with a tiny control wrapper and read the expansion.

Start With the Desired Call Site

Write the call site first:

1(with-timing "load users"
2  (println "loading users")
3  (->> (load-users db)
4       (filter active?)))

This syntax says: run a body, measure elapsed time, print a label, and return the body’s result. A function cannot receive the body before it evaluates, so a macro can be justified.

Write the Macro

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

What to notice:

Part Why it is there
& body The macro accepts one or more caller forms.
~@body The body forms are spliced into a do.
result# The body result is captured once and returned.
started# and elapsed-ms# Auto-gensyms avoid collisions with caller locals.
~label The caller’s label expression is inserted into generated code.

The macro is still small. If this grew into logging policy, exception handling, metrics publishing, and retry behavior, it would become harder to justify.

Inspect the Expansion

 1(macroexpand-1
 2  '(with-timing "load users"
 3     (println "loading users")
 4     (->> (load-users db)
 5          (filter active?))))
 6
 7;; roughly:
 8(let [started__auto__ (System/nanoTime)
 9      result__auto__ (do
10                       (println "loading users")
11                       (->> (load-users db)
12                            (filter active?)))
13      elapsed-ms__auto__ (/ (- (System/nanoTime) started__auto__)
14                            1000000.0)]
15  (println "load users" "took" elapsed-ms__auto__ "ms")
16  result__auto__)

The generated names will differ, but the shape should be clear: evaluate the body once, print timing, return the body result.

Compare With a Function

A higher-order function can time an already packaged function:

 1(defn timed
 2  [label f]
 3  (let [started (System/nanoTime)
 4        result (f)
 5        elapsed-ms (/ (- (System/nanoTime) started) 1000000.0)]
 6    (println label "took" elapsed-ms "ms")
 7    result))
 8
 9(timed "load users"
10       #(do
11          (println "loading users")
12          (->> (load-users db)
13               (filter active?))))

This function version is often good enough. The macro only wins if the body syntax is materially clearer and the team accepts the extra expansion/debugging cost.

Review Before Reuse

Question Acceptable answer
Does this macro control evaluation? Yes, it wraps unevaluated body forms.
Does the body run once? Yes, through one (do ~@body) bound to result#.
Are local names safe? Yes, auto-gensyms are used for generated locals.
Could a function work? Yes, but with a less direct #(do ...) call site.
Is the behavior hidden? No, the expansion is ordinary let, do, and println.

Knowledge Check

### Why can `with-timing` reasonably be a macro? - [x] It needs to wrap unevaluated body forms and return their result. - [ ] It makes `System/nanoTime` faster. - [ ] It avoids all runtime allocation. - [ ] It prevents exceptions automatically. > **Explanation:** A function receives evaluated arguments. A macro can place the body forms inside generated timing code before they run. ### Why does the macro bind `result#`? - [x] To evaluate the body once and return that same value. - [ ] To convert the result to Java bytecode manually. - [ ] To force the result to be a string. - [ ] To disable macro expansion. > **Explanation:** Binding the body result prevents duplicated evaluation and preserves the normal return value of the wrapped body. ### When might the `timed` function be better than the macro? - [x] When passing a zero-argument function is clear enough for the team. - [ ] When you need to control whether body forms evaluate. - [ ] When generated code must introduce bindings. - [ ] When macro expansion must be inspected. > **Explanation:** Functions are easier to test and debug. If `#(do ...)` is acceptable, the function version may be the simpler abstraction.
Revised on Saturday, May 23, 2026