Clojure
2026-08-20 10:30
your entire Clojure program is a series of lists: the very source code of your program is interpreted by the Clojure compiler as lists
Unit 2 added: cond is a nested list of pairs, and generating a list is easy.
Everything so far has been about using a language.
This is about changing it.
unless exampleThe phases of the Clojure runtime — the separation that makes macros possible.
Read — text becomes data structures. Reader macros act here.
Macroexpand — macros are called with unevaluated forms and return forms.
Compile — the result becomes bytecode.
Run — the bytecode executes.
It is an ordinary function that builds a list.
The first element is the quoted symbol dosync.
That list is the code that will run.
Arguments are forms, not values.
(sync-set a-ref 1)hands the macro the symbola-ref, not the ref.
Macros have no runtime existence.
They cannot be passed to
map, stored, or called dynamically. They are not values.
It printed anyway.
Clojure evaluates arguments before calling.
By the time unless receives then, the println has already run.
The function only sees the nil it left behind.
Invisible when the body is pure.
Visible only when the body has a side effect or is expensive —
which is exactly when you care.
Works — and every caller must remember the #().
Forget it once and the bug is back, silently.
The body was never evaluated. It was moved.
macroexpand-1 — one stepmacroexpand — until the outermost form is not a macromacroexpand-all — everything, including nestedNone of them run the result.
A function receives values. A macro receives forms.
Must it decide whether to evaluate? Not a function.
Must it decide when, or how many times? Not a function.
Otherwise — use a function.
Two differences from ':
user/then — the macro’s own parameter name, inserted literally.
Printed both, then threw.
Double parentheses. Both printlns evaluate to nil, giving (nil nil) —
an attempt to call nil as a function.
~ inserts the sequence. ~@ inserts its elements.
Suppose now were not qualified:
daily-report would see a number, not "2009-10-22".
The macro’s let captured the caller’s now.
now# might expand to now_14187_auto_.
Every occurrence in the template gets the same generated symbol.
Syntax quote’s namespace resolution → prevents capture of function names
Auto-gensym → prevents capture of local bindings
The whole implementation. Returns nil.
Proof that arguments really are unevaluated — the body can be nonsense.
Several forms from a variable number of arguments — that is ~@.
Cannot be a function: it must not evaluate expr when the name already exists.
Short-circuits · recursive · gensym so x is evaluated once
And it explains why and returns the deciding value, not a boolean.
Must be a macro: it needs the form unevaluated in order to time evaluating it.
Every technique from the previous section, in production code.
Each macro exists for a reason expressible in one sentence.
None of them is a macro for style.
No evaluation control. No bindings. Purely rearranging.
A macro is, at bottom, a function from a list to a list.
Must be a macro — a function would evaluate all three.
That destructuring preamble repeats in every handler.
The shape behind every def-something in every library you will use.
The macro computes part of the code, not merely templates it.
The message quotes the source you wrote.
A function receives only false, and can say nothing about where it came from.
Every testing library depends on this trick.
If a function can do it, use a function.
map, stored, or composedUsed well, a macro removes a repeated shape no function could abstract.
Used badly, it makes ordinary code unreadable for no gain.
unless cannot be a function because arguments evaluate first.More on Functional Programming sets macros aside.
Higher-order functions and closures, pushed until they become an object system.
And delayed evaluation from the other side — what a closure can do that a macro cannot.
Clojure · Evolving Clojure Through Macros