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.
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.
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.
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.
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.
| 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. |