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