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:
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 lastlet — 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 recursionthrow, try / catch / finally — exceptionsnew, ., set! — Java interop and the few allowed mutationsquote, var — code-as-data and var referencesYou 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 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))
macroexpand-1 When You’re ConfusedWhen 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))