Clojure
2026-08-20 10:00
Collections, defn, let, conditionals, sequence functions, threading macros.
It did not stop to explain the machinery.
This unit stops.
Work through it with a REPL open. Expect to come back to it.
Closures → the later unit builds an object system from them.
Code as data → the macro units depend on it.
Tag a map, list or vector with other data —
without changing the value of the tagged data.
Immutable values compare by content.
[1 2 3] and [1 2 3] are the same.
But sometimes it matters that this one came from an untrusted source.
Metadata provides a way to add identity to values when it matters.
This tags the list starting with hash-map — the code.
Not the map the call produces.
The metadata becomes invisible at runtime. Nothing errors.
Preserving the identity semantics.
Java verifies types at compile time.
Clojure is dynamically typed — often the type is not known until runtime.
So the JVM uses reflection to find the method. That works, and it is slow.
And no reflection warning.
^String is stored as {:tag String}.
Same mechanism as the previous section.
(set! *warn-on-reflection* true)Concentrate on arguments and return values — Clojure infers the body.
^byte for the primitive · ^bytes for the array
try expressions evaluate in order; the last one’s value is returnedcatch runs and its value is returnedfinally always runs — and never returns anythingNot the better match — the first matching clause wins, and ArithmeticException is a RuntimeException.
Arrange catch clauses most specific first.
And note: try is an expression returning a value.
That is what lets a later unit wrap it in a closure.
The docstring attaches as metadata on the var — which is how doc finds it.
% is the return value in :post.
How Clojure does optional parameters without having optional parameters.
& collects the rest. This is how str and + are written.
The JVM has no tail-call elimination.
Rebinds and jumps rather than calling. No stack growth.
Only works for self-recursion in tail position.
Compiles — and still blows the stack. recur cannot cross two functions.
Return a function to call next:
trampoline calls in a loop, so the stack never grows.
Self-recursive, tail position → recur
Mutually recursive → trampoline
Neither → accept the depth, and know your limit
some returns the first logical-true result — so it can find, not just test.
zero?, then not, then str. Read it backwards.
Same result. No fn.
A one-line change for an expensive pure function.
And a trap if it is not pure.
(def users
[{:username "kyle" :firstname "Kyle" :lastname "Smith"
:balance 175.00M :member-since "2009-04-16"}
{:username "zak" :firstname "Zackary" :lastname "Jones"
:balance 12.95M :member-since "2009-02-01"}
{:username "rob" :firstname "Robert" :lastname "Jones"
:balance 98.50M :member-since "2009-03-30"}])A function with a boolean flag anticipates two cases.
A function taking a function anticipates none —
it works for cases you never thought of.
With nothing left over, %& is nil, not an empty sequence.
It cannot nest — an inner % would be ambiguous.
It hides parameter names, which often document intent.
#() for a glance. fn once it needs explaining.
Same result. No anonymous function at all.
Lexical — determined by where the name appears in the source.
Work it out by reading. No running required.
Dynamic — determined by the call stack at runtime.
You cannot know it from the function alone.
Lexical.
let bindings and function parameters.
Dynamic scope exists and must be asked for explicitly.
Because it makes behaviour depend on context invisible at the definition.
expense-report never mentions the caller and still sees "production".
Aspect-oriented logging. No framework.
A var’s root binding is visible to all threads.
A binding override is visible to none but the current one.
Twenty was set. Ten was used.
map is lazy. The sequence is realised when the REPL prints it —
by which time execution has left the binding.
let makes a new lexical binding. multiply refers to the var, untouched.
scale is neither the inner function’s parameter nor bound in its body.
It is a free variable, and the function closes over it.
create-scaler returned long ago. Its scale is still there.
This is what the later unit turns into an object system.
parse is unqualified — and you cannot tell where it came from.
Now the name says where it comes from. Prefer this.
Namespaces can be created, switched and inspected while the program runs.
Not exotic — it is what makes REPL-driven development work.
The interface is now visible in the first line.
:or is how optional arguments are normally done.
Anywhere a binding appears.
let · function parameters · doseq · for · most macros
#inst · #uuid
A tag plus a form, handled by the reader — before evaluation —
via a function registered for that tag.
The data format. Not the syntax.
You are not adding grammar. You are saying how tag-plus-form becomes a value.
Which is why tagged literals travel safely between programs.
defn is data the reader shapes into a definitionAll three depend on Clojure code being data.
The macro units make that the entire subject.
recur for self, trampoline for mutual.binding is dynamic, thread-local, and bites with laziness.State and the Concurrent World takes a different thread —
the value/identity distinction from unit 1 —
and makes it concrete: refs and STM, agents, atoms, vars,
one unified access model, and futures and promises.
Clojure · Building Blocks of Clojure