Browse Learn Clojure Foundations as a Java Developer

Compose and Recur in Clojure Macros Safely

Learn how to compose small Clojure macros, avoid unreadable macro chains, and use recursive code generation only when a data-driven builder is not clearer.

Macro composition means one macro expands into code that may include another macro call or uses helper functions to build generated forms. Macro recursion means a macro uses recursive logic to generate repeated code. Both techniques raise the review cost quickly.

The safer rule is: compose helpers freely, compose macros cautiously.

Prefer Helper Functions for Form Building

Instead of making every generation step a macro, use ordinary functions that return forms.

1(defn logging-form
2  [level message]
3  `(println ~(str "[" (name level) "]") ~message))
4
5(defmacro log-at
6  [level message]
7  (logging-form level message))

The macro boundary stays small. The helper function can be tested as data transformation.

Part Why this is safer
logging-form Ordinary function that returns a form.
log-at Thin macro wrapper around call-site syntax.
Expansion Easy to inspect with macroexpand-1.

Avoid Deep Macro Chains

A macro that expands into another macro is sometimes fine, but long chains make debugging harder.

1(defmacro unless
2  [condition & body]
3  `(when-not ~condition
4     ~@body))

This is acceptable because unless is a tiny alias over a familiar control form. A five-step chain of custom macros is different: error messages and expansion review become much harder.

Recursive Generation

Use recursive generation when the output is repetitive and structurally regular. Prefer a function that builds forms, then let the macro splice the result.

 1(defn defgetter-form
 2  [field]
 3  `(defn ~(symbol (str "get-" (name field)))
 4     [m#]
 5     (get m# ~field)))
 6
 7(defmacro defgetters
 8  [& fields]
 9  `(do
10     ~@(map defgetter-form fields)))

The macro has one job: splice generated definitions into a do. The repeated generation happens in a normal function.

1(macroexpand-1
2  '(defgetters :id :email))
3
4;; roughly:
5(do
6  (defn get-id [m__auto__] (get m__auto__ :id))
7  (defn get-email [m__auto__] (get m__auto__ :email)))

Composition Risk Table

Pattern Use with confidence? Reason
Function builds one form Yes Testable as data transformation.
Thin macro wraps one helper Usually Macro boundary remains small.
Macro expands into a built-in macro Usually Expansion stays familiar.
Macro expands into several custom macros Caution Debugging and errors become indirect.
Recursive macro with no clear base case No Expansion can be confusing or fail badly.

Java Translation

Java pattern Clojure macro equivalent
Code generator templates Helper functions returning forms.
Annotation processor emitting many methods Macro splicing multiple generated defn forms.
Builder chain hiding work Macro chain that hides expansion steps.

Review Checklist

Check Good answer
Can the form-building logic be a function? Yes, most of it is.
Is the macro boundary thin? Yes, it mainly controls call-site syntax.
Does recursive generation terminate visibly? Yes, it maps over finite input or has a clear base case.
Does macroexpand-1 show readable generated code? Yes, without needing five more manual expansions.

Knowledge Check

### Why prefer helper functions that return forms? - [x] They are ordinary functions and can be tested as data transformations. - [ ] They prevent all compile errors. - [ ] They force macro expansion at runtime. - [ ] They remove the need for syntax quote. > **Explanation:** Keeping form-building logic in functions reduces the macro boundary and makes most of the generator easier to test. ### What is risky about deep custom macro chains? - [x] Expansion review and error messages become indirect. - [ ] Clojure forbids macros expanding into macros. - [ ] They always run slower at runtime. - [ ] They cannot use Java interop. > **Explanation:** A small macro over a familiar built-in is readable. Long chains of custom macros make it harder to know which generated form caused a problem. ### What does `defgetters` use `~@` for? - [x] To splice a sequence of generated `defn` forms into a `do`. - [ ] To dereference fields. - [ ] To quote the field list as data. - [ ] To prevent namespace loading. > **Explanation:** `~@` inserts each generated form into the surrounding syntax-quoted `do`.
Revised on Saturday, May 23, 2026