Browse Learn Clojure Foundations as a Java Developer

Prefer Functions Before Clojure Macros

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.

The Function-First Ladder

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.

Replace Body Syntax With a Thunk

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.

Use Data Before Syntax

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.

Use Dispatch Instead of Generated Branches

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.

When a Macro Still Wins

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.

Review Questions

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.

Knowledge Check

### Why are functions the default abstraction in Clojure? - [x] They are easier to test, compose, and reason about than macros. - [ ] They can control unevaluated syntax. - [ ] They always run at compile time. - [ ] They replace all Java interop. > **Explanation:** Functions operate on values and fit normal runtime tooling. Use macros only when functions cannot express the call shape. ### What does a zero-argument function help replace? - [x] A macro whose only job is to wrap a body of code. - [ ] A namespace declaration. - [ ] A data map. - [ ] A Java classpath entry. > **Explanation:** Passing `#(...)` lets a function decide when to call the body, often avoiding a macro. ### When should data be preferred over a macro-backed DSL? - [x] When the problem is configuration or a declarative plan. - [ ] When syntax evaluation must be delayed. - [ ] When generated bindings are required. - [ ] When callers need a new control-flow form. > **Explanation:** Plain data is easier to inspect, transform, serialize, and test. Use macro syntax only when data cannot express the needed shape safely.
Revised on Saturday, May 23, 2026