Clojure
2026-08-20 10:45
The previous unit changed the language.
This one goes back to using it —
and takes one idea much further than before.
By the end you will have built an object system in Clojure.
Private state. Message dispatch.
From nothing but closures.
Then we ask whether you should.
Identical except for one function call.
You have written map.
cons wraps the recursion, so it is not in tail position.
recur cannot help.
The recursive call waits until the next element is asked for.
Same shape — differing only in how they combine.
You have written reduce.
The accumulator is a vector, not a number.
Identical to all-greater-than but for the comparison. Extract it:
You have written filter.
Every one came from noticing that two functions differed in one place —
and making that place a parameter.
Now you need California. Then New York. Then everywhere.
Duplication, and a new function per jurisdiction.
Each result is an ordinary one-argument function.
Adapting a general function by fixing its varying argument.
The returned function is a closure over state-tax.
Clojure has this as partial, used identically.
You can only fix arguments from the left.
Put the parts that vary least first.
Which is why (map f coll) is ordered as it is.
x is neither a parameter nor bound in the body. It is free.
A function with free variables is open —
you cannot say what it computes without knowing those names.
Capturing them closes it.
adder returned long ago, and x is still there(adder 2 3) and (adder 10 20) are independentadd-5 takes one argument, not threeConfigured at creation, not parameterised at every call.
Wrapping in a function of no arguments postpones it.
A function can now receive unevaluated work —
otherwise the exclusive privilege of macros.
Exception handling has become a value.
Closure — the caller writes the wrapper. Visible at the call site. Result is a value.
Macro — rewrites code. Caller writes ordinary code. Not a value.
The trade is syntax versus composability.
unless had to be a macro — requiring #() everywhere would be intolerable,
and easy to forget.
Here, where the wrapper is a deliberate abstraction,
the extra #() buys you a value back.
The inner function can see password. Callers cannot.
Although
arjunis a function, semantically it looks and behaves like data.
State ✓ · Behaviour ✓
Built up with methods, state and inheritance: a little over 50 lines.
Are objects a poor man’s closures, or is it the other way around?
Fewer than half manipulate functions.
The rest exist to make the syntax look a certain way.
The semantics would not change under a different syntax.
in most cases, such artificial constructs are unnecessary in languages such as Clojure
You do not need objects for data abstraction.
Clojure’s core structures are immutable and thread safe —
no need for procedural abstractions wrapping their mutation.
it’s better to have 100 functions that operate on a single data structure instead of 10 functions that operate on 10 data structures
creating an object system … raises a barrier of inoperability with other libraries that don’t know about it
A map is transparent, printable, comparable, serialisable.
A closure’s state is opaque.
Because the exercise establishes the equivalence.
Deciding to use maps because objects are unavailable is not a decision.
Deciding to use maps having built objects is.
More Macros and DSLs returns to the macro thread —
with the same shape of ambition.
Anaphoric macros · compile-time computation
macros that write macros · a domain-specific language
Clojure · More on Functional Programming