Browse Clojure Foundations for Java Developers

Special Forms and Macros

Recognize the small set of special forms and understand what macros do when reading real Clojure code.

Most Clojure code is “just” function calls over persistent data. The two big exceptions you must recognize when reading code are:

  • special forms (built-in evaluation rules)
  • macros (compile-time transformations of code-as-data)

Special Forms: The Few Forms With Custom Evaluation

In Clojure, function arguments are evaluated before the function runs. Special forms exist when that rule doesn’t work.

Example: if must not evaluate both branches.

These are the special-form shapes you’ll see most often in everyday code (written directly or via macros that expand to them):

  • if — conditional branching (returns a value)
  • do — evaluate multiple expressions, return the last
  • let — local bindings (you’ll sometimes see let* in macro expansion)
  • fn — function literal (you’ll sometimes see fn* in macro expansion)
  • loop / recur — efficient local recursion
  • throw, try / catch / finally — exceptions
  • new, ., set! — Java interop and the few allowed mutations
  • quote, var — code-as-data and var references

You don’t need to memorize the full list on day one. The goal is to recognize: “this isn’t a normal function call; evaluation rules are different here.”

Macros: “Write Code That Writes Code”

Macros receive unevaluated forms (data that represents code) and return new forms that get compiled.

The practical reason macros exist: they let you express patterns that need control over evaluation or syntax.

For example, when looks like a control structure, but it’s a macro that expands to an if + do shape:

1(when ok?
2  (println "ok")
3  :done)

Roughly expands to:

1(if ok?
2  (do
3    (println "ok")
4    :done))

Reading Tip: Use macroexpand-1 When You’re Confused

When a form feels “magical,” expanding it usually reveals that it’s built out of small parts you already know.

1(macroexpand-1 '(when ok? :done))

Knowledge Check: Special Forms and Macros

### Why can’t `if` be an ordinary function? - [x] Because a function would evaluate all arguments before it runs, but `if` must evaluate only one branch. - [ ] Because functions can’t return values. - [ ] Because `if` requires mutable state. - [ ] Because Java interop only works in special forms. > **Explanation:** Normal functions can’t control evaluation of their arguments. `if` must be able to skip evaluating the branch you don’t take. ### What does a macro receive as input? - [x] Unevaluated forms (code represented as data) - [ ] Only strings - [ ] Only already-evaluated values - [ ] JVM bytecode instructions > **Explanation:** A macro runs during expansion/compilation, takes forms, and returns a new form that will later be evaluated. ### What is `macroexpand-1` most useful for? - [x] Seeing what a macro expands into so you can understand or debug the resulting code. - [ ] Running a macro in a separate thread. - [ ] Optimizing code by inlining JVM bytecode. - [ ] Evaluating a form twice for benchmarking. > **Explanation:** Expanding one step often reveals familiar building blocks (`if`, `do`, `let`) under a macro-heavy surface syntax.
Revised on Friday, April 24, 2026