Clojure
2026-08-20 11:00
Last unit: closures pushed until they became objects — then we chose not to use them.
This unit: macros pushed until they become languages.
The earlier macro unit added unless, randomly, assert-true.
This one:
When a problem is awkward in your language —
build a better one for it.
`, ~, ~@macroexpand shows what was producedname# prevents captureThis unit builds on all of it and re-derives none of it.
The earlier unit taught that an un-gensymed binding is a bug.
Variable capture.
Hold onto that. The next section does it on purpose.
A word referring back to something earlier.
take the report and file it
It runs twice.
Better — but you invented a name and wrote it three times.
~'it inserts the unqualified symbol.
A bare it would become user/it and be useless.
Deliberately unhygienic.
The binding is invisible at the call site.
A reader who does not know the macro cannot tell where it came from.
And any it the caller already had is shadowed.
That #() exists only to move the value into the right slot.
Each step binds to result. Put it where it belongs.
Each step binds it and recurses — nested let forms.
(def ALPHABETS [\a \b \c ... \z])
(def NUM-ALPHABETS (count ALPHABETS))
(def INDICES (range 1 (inc NUM-ALPHABETS)))
(def lookup (zipmap INDICES ALPHABETS))
(defn shift [shift-by index]
(let [shifted (+ (mod shift-by NUM-ALPHABETS) index)]
(cond
(<= shifted 0) (+ shifted NUM-ALPHABETS)
(> shifted NUM-ALPHABETS) (- shifted NUM-ALPHABETS)
:else shifted)))shifted-tableau runs on every call —
though it depends only on a constant.
memoize helps, but it still runs at least once.
The table written into the code.
Computes the tableau during expansion and embeds the result.
An inline literal map.
Macros expand during reading; the generated tableau lands in the source as a literal.
the new
encrypt13function at runtime doesn’t do any tableau computation at all
Ship it as a library and users would never know shifted-tableau was called.
Work that is expensive, repeated, and determined by compile-time constants.
All three. And no function can do it, at any level of cleverness.
A macro returns code.
A defmacro is code.
So a macro can return a defmacro.
Written by hand, b would be:
So make-synonym must produce exactly that.
user/stuff and user/old-name. Both wrong.
One backquote produces the form.
Two produce code that produces the form — hence all the concat and list.
~'~old-namefirst,
~old-nameis expanded, leaving~'binding… Then the outer backquote is expanded, leaving'binding, which finally becomes(quote binding)
Needed so the value is not resolved until the generated macro expands.
Not by reasoning about quote levels in your head.
Macroexpand one level at a time and compare against the hand-written template.
Not for one macro. Write that macro.
It pays for a family of near-identical macros —
which is exactly what a DSL is.
Decomposition — top-down breaks the problem down; bottom-up builds vocabulary up.
Combinability — vocabulary you can combine covers cases you did not anticipate.
Segment users of a website by what they did.
(ns clj-in-act.ch11.session
(:require redis))
(def redis-key-for :consumer-id)
(def ^:dynamic *session*)
(defn save-session [session]
(redis/set (redis-key-for session) (pr-str session)))
(defn find-session [consumer-id]
(read-string (redis/get consumer-id)))
(defmacro in-session [consumer-id & body]
`(binding [*session* (find-session ~consumer-id)]
(do ~@body)))They say what a segment is.
Not how to compute it.
No Redis. No sessions. No lookups.
Only two things.
defsegment — it defines a name and must receive the rule unevaluated.
in-session — it establishes a binding around a body.
Everything else is ordinary functions and data.
A DSL built mostly from macros is usually a DSL built wrong.
When a problem is awkward, build a better language for it.
Most languages do not offer this option.
Which makes the discipline of not doing it part of the skill.
A good DSL makes the problem visible instead of the plumbing.
And lets people who understand the domain read and check the rules.
Would a competent Clojure programmer, new to this codebase, be able to read it?
Easier because the domain vocabulary is clearer? Build it.
Harder because they must learn your language first? Write the functions.
thread-it threads into any position, not only first or last.From reading your first parenthesis —
to building languages.
True since unit 1:
Your program is data, and you can compute with it.
Clojure · More Macros and DSLs