Compare functions, higher-order functions, data-driven design, protocols, multimethods, and Java interop as safer alternatives before writing a custom Clojure macro.
Most abstractions in Clojure should be functions, not macros. Functions are easier to test, compose, mock, profile, and explain to a mixed Java/Clojure team.
Use a macro only after the normal Clojure tools fail for a specific reason.
| Try first | Use it when |
|---|---|
| Plain function | Arguments can be evaluated before the abstraction runs. |
| Higher-order function | You need caller-provided behavior. |
| Data map or vector | You are describing configuration or a plan. |
| Protocol or multimethod | Dispatch varies by type, value, or domain category. |
| Java interop | Existing JVM code already solves the runtime problem. |
| Macro | The call site needs syntax or evaluation control. |
This order is not anti-macro. It keeps macros for the cases where they are actually stronger.
Many macro ideas can become functions if the caller passes a zero-argument 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 #(->> (load-users db)
11 (filter active?)))
The call site is slightly noisier than a macro, but the abstraction is an ordinary function. That is usually a good trade for team code.
If the “DSL” is really configuration, use data.
1(def report-spec
2 {:source :orders
3 :filters [[:status := :open]
4 [:total :> 1000]]
5 :select [:id :customer :total]})
Data is inspectable, transformable, serializable, and testable without macro expansion. Java engineers moving from builders and annotations often underestimate how far plain Clojure data can go.
If behavior varies by kind, reach for a function map, multimethod, or protocol before generating code.
1(def handlers
2 {:created handle-created
3 :paid handle-paid
4 :cancelled handle-cancelled})
5
6(defn handle-event
7 [event]
8 (if-let [handler (get handlers (:type event))]
9 (handler event)
10 (throw (ex-info "Unknown event type" {:event event}))))
This keeps extension points explicit. A macro that generates a hidden dispatch table would be harder to inspect and test.
| Requirement | Function alternative | Macro judgment |
|---|---|---|
| Wrap a body for timing/logging | Pass #(do ...) |
Function unless body syntax is central. |
| Add resource cleanup around a body | Pass a thunk | Macro if binding syntax is materially clearer. |
| Conditional body execution | Pass a thunk or use built-ins | Macro only for a reusable syntax form. |
| Build configuration | Use data | Macro only if data cannot express valid syntax safely. |
Before writing defmacro, ask:
| Question | If yes |
|---|---|
| Can I pass a function instead of body forms? | Prefer a function. |
| Can I pass data instead of inventing syntax? | Prefer data. |
| Can a protocol or multimethod model variation? | Prefer dispatch. |
| Is the macro only hiding boilerplate? | Keep it out of shared code unless the expansion is tiny. |