Browse Clojure Foundations for Java Developers

Macros and Metaprogramming

Understand macros as compile-time code transformation, and learn when to prefer functions instead.

Macros are one of the features that make Clojure feel like a real language toolkit rather than just a library ecosystem. A macro receives code as data, transforms it, and returns new code to evaluate.

That sounds exotic, but most of the macros you use at first are ordinary parts of the language:

  • when
  • cond
  • ->
  • ->>
  • doseq
  • comment

The important thing for a Java engineer is not “macros are magical.” It is “macros exist because some problems are about shaping code, not just processing values.”

Macro Vs Function: The Practical Difference

This is the definition that matters:

  • a function receives evaluated values
  • a macro receives unevaluated forms and returns a new form

That means a macro can control evaluation and syntactic shape in a way a normal function cannot.

For example, when only evaluates its body when the condition is truthy. If when were a normal function, the body expressions would already have been evaluated before the function call.

A Small Macro Example

1(defmacro unless [test & body]
2  `(if (not ~test)
3     (do ~@body)))

This macro creates a new control form: “run the body unless the test is true.”

1(macroexpand-1 '(unless ready? (println "not ready")))
2;; => (if (clojure.core/not ready?) (do (println "not ready")))

If you are ever unsure what a macro is doing, macroexpand-1 is the first debugging tool to reach for. It shows the transformed code shape before evaluation.

Why Functions Should Still Be Your Default

Most of the time, functions are the right tool:

  • they are easier to test
  • they are easier to compose
  • they operate on runtime values directly
  • they are easier for other engineers to read and debug

Prefer a function when you want to:

  • transform data
  • parameterize behavior
  • reuse runtime logic
  • hide ordinary implementation detail

Reach for a macro only when a function cannot express the desired shape cleanly.

What Macros Are Good At

Macros earn their keep when you truly need one of these:

  • control over when something is evaluated
  • a new syntactic shape that reads better than the raw expansion
  • compile-time generation of repeated code patterns

This is why built-in forms like when and threading macros feel natural: they improve the syntax of common patterns without forcing you to write everything as nested function calls.

A Better Java Mental Model

Macros are closer to compile-time AST transformation than to runtime reflection.

That comparison is not perfect, but it is directionally useful. You are not “calling a clever function.” You are extending or reshaping the language before evaluation happens.

This is also why macros should be treated with care. They change how code is written and read, not just how data is processed.

Common Overuse Patterns

Java developers new to macros sometimes use them too early because they look powerful. Common mistakes include:

  • hiding ordinary function calls behind custom syntax
  • using a macro to avoid passing data explicitly
  • creating DSLs before the plain functions are well understood

Those choices usually make the codebase harder to maintain. A macro may save a few characters while making the behavior much harder to trace.

A Good Rule Of Thumb

Ask these questions in order:

  1. Can this be a plain function?
  2. Can it be data plus a plain function?
  3. Do I actually need to control evaluation or generate code shape?

If the answer to the first two is yes, stop there.

Macros are valuable in Clojure precisely because they are not the default answer. They are a precision tool for language extension, not a replacement for good function design.

Knowledge Check: Macros

### What is the key difference between a macro and a function? - [x] Macros transform code forms before evaluation; functions operate on evaluated values. - [ ] Macros always run faster than functions. - [ ] Macros can’t call other functions. - [ ] Macros can’t take arguments. > **Explanation:** A macro operates on the shape of code before evaluation. A function operates on values after argument evaluation has already happened. ### What does `macroexpand-1` help you do? - [x] See the code a macro produces before it is evaluated. - [ ] Run macro code faster at runtime. - [ ] Convert a macro into a Java class. - [ ] Prevent evaluation of any form. > **Explanation:** Looking at the expansion is the fastest way to understand the real code a macro produces. ### When is a macro most justified? - [x] When you need a new syntactic shape or control structure that can’t be expressed cleanly as a function. - [ ] Whenever you want to avoid writing tests. - [ ] Whenever you want to remove all parentheses. - [ ] Whenever you want inheritance-based polymorphism. > **Explanation:** Prefer functions first. Use a macro when the problem is really about evaluation or code shape, not ordinary runtime data processing.
Revised on Friday, April 24, 2026