Lecture notes — Building Blocks of Clojure

Published

2026-08-20 00:00

Keywords

ver. 1.0.0

← Building Blocks of Clojure

Where we are

In Clojure Elements: Data Structures and Functions you got a working vocabulary — collections, defn, let, the conditionals, the sequence functions and the threading macros. It moved fast and deliberately did not stop to explain the machinery.

This unit stops. It is the reference chapter of the module, and it works through the parts a real program needs: metadata and type hints, exceptions, functions in full, scope in both flavours, namespaces, destructuring and reader literals.

Two things here recur later. Closures are what the later functional-programming unit builds an object system from. And code as data — visible in metadata, in what defn expands to, and in reader literals — is what the macro units depend on.

Work through it with a REPL open, and expect to come back to it.

What you will be able to do

  1. attach-and-read-metadata — Attach metadata to a value and explain why it does not change that value.
  2. use-java-type-hints — Add type hints to avoid reflection, and measure the difference.
  3. handle-exceptions — Catch and throw JVM exceptions from Clojure.
  4. define-functions-in-full — Define functions with multiple arities, variadic parameters, and pre/post conditions.
  5. write-recursive-functions — Write self-recursive and mutually recursive functions in Clojure.
  6. use-core-higher-order-functions — Use the core higher-order functions: every?, some, constantly, complement, comp, partial and memoize.
  7. write-higher-order-functions — Write your own function that takes or returns a function.
  8. write-anonymous-functions — Write anonymous functions with fn and with the #() reader macro.
  9. use-callable-data-structures — Use keywords, maps and vectors in function position.
  10. distinguish-lexical-and-dynamic-scope — Distinguish lexical scope from dynamic scope and say which Clojure uses where.
  11. use-vars-and-dynamic-binding — Create dynamic vars and rebind them per thread with binding.
  12. write-lexical-closures — Write a closure and identify its free variables.
  13. organise-code-with-namespaces — Organise code into namespaces with the ns macro and manipulate them at runtime.
  14. destructure-bindings — Destructure vectors and maps inside any binding form.
  15. explain-reader-literals — Explain what a reader literal is and define one of your own.

What we will cover

  • Metadata — data about data, attached without changing the value.
  • Type hints — telling the compiler a class so it need not use reflection.
  • Exception handlingtry, catch, finally and throw on the JVM.
  • Functions and arities — multiple arity, variadic parameters, pre/post conditions.
  • Recursion — self-recursive and mutually recursive, with recur and trampoline.
  • Higher-order functions — using the core ones and writing your own.
  • Callable data structures — keywords, maps and vectors in function position.
  • Lexical scope and closures — capture, and what a closure carries.
  • Dynamic scope and bindings — vars, binding, thread-locality and laziness.
  • Namespaces — organising code and manipulating it at runtime.
  • Destructuring — pulling structures apart inside a binding form.
  • Reader literals — extending the reader with tagged literals.

Going underneath

Five areas, in order:

  • Metadata and type hints — tagging data without changing it, and removing reflection
  • Exceptions — because everything after this can fail
  • Functions in full — arities, recursion, the core higher-order functions, writing your own, anonymous forms
  • Scope — lexical versus dynamic, vars and binding, let revisited, closures
  • Namespaces, destructuring and reader literals — organising, unpacking, extending

Learning outcomes

  • attach-and-read-metadata: Attach metadata to a value and explain why it does not change that value.
  • define-functions-in-full: Define functions with multiple arities, variadic parameters, and pre/post conditions.
  • distinguish-lexical-and-dynamic-scope: Distinguish lexical scope from dynamic scope and say which Clojure uses where.

Metadata

Metadata means data about data. Clojure supports tagging data — maps, lists, vectors — with other data without changing the value of the tagged data. Concretely: the same values with different metadata still compare equal.

Why this exists

The point of immutable values is that you compare them by content rather than identity. [1 2 3] and [1 2 3] are the same even at different memory addresses, so it does not matter which one your program uses.

But in the real world you often need to distinguish otherwise identical things. One value may equal another, and it still matters that one came from an untrusted network source or a file with a specific name.

Metadata provides a way to add identity to values when it matters.

Attaching it

(def untrusted (with-meta {:command "delete-table" :subject "users"}
                          {:safe false :io true}))

The map now has a metadata map attached with keys :safe and :io. Metadata is always a map, and it is attached on the outside:safe and :io are never added as keys to the original map.

There is a reader-macro shorthand, ^{}, which attaches at read time rather than eval time:

(def untrusted ^{:safe false :io true} {:command "delete-table" :subject "users"})
ImportantA silent trap in the read-time form

This is not the same thing:

(def untrusted ^{:safe false :io true} (hash-map :command "delete-table" :subject "users"))

It associates the metadata with the list starting with hash-map — the code — not with the hash map that call produces. The metadata becomes invisible at runtime.

Nothing errors. Worth doing at a REPL once so you recognise the symptom.

Reading it back

Objects with metadata behave like any other object. It does not even show at the REPL:

untrusted
;=> {:command "delete-table", :subject "users"}

And it does not affect equality:

(def trusted {:command "delete-table" :subject "users"})
(= trusted untrusted)
;=> true

Use meta to see it:

(meta untrusted)
;=> {:safe false, :io true}
(meta trusted)
;=> nil

Metadata is carried forward. When new values are created from tagged ones, the metadata comes along — preserving the identity semantics:

(def still-untrusted (assoc untrusted :complete? false))
still-untrusted
;=> {:complete? false, :command "delete-table", :subject "users"}
(meta still-untrusted)
;=> {:safe false, :io true}

Functions and macros can carry metadata too, which is how docstrings and privacy markers work — the namespace section returns to that.

Learning outcomes

  • attach-and-read-metadata: Attach metadata to a value and explain why it does not change that value.

Concepts

  • metadata: defines metadata, the reader macro for it, and the functions that read it back

Type hints and the cost of reflection

A Java type hint is metadata stored in the :tag key, used often enough to have its own reader macro syntax: ^symbol.

Why it is needed

When you make a Java method call, the JVM needs to know which class defines the method so it can find the implementation. In Java that is verified at compile time. Clojure is dynamically typed, so often the type is not known until runtime — and then the JVM must use reflection to determine the class and find the method.

That works. It is slow. And you can see exactly how slow:

(set! *warn-on-reflection* true)
;=> true

(defn string-length [x] (.length x))
Reflection warning, reference to field length can't be resolved.

(time (reduce + (map string-length (repeat 10000 "12345"))))
"Elapsed time: 45.751 msecs"
;=> 50000

Now add the hint:

(defn fast-string-length [^String x] (.length x))
;=> #'user/fast-string-length

(time (reduce + (map fast-string-length (repeat 10000 "12345"))))
"Elapsed time: 5.788 msecs"
;=> 50000

Roughly eight times faster, and no reflection warning.

The hint really is metadata

You can go and look at it:

(meta (first (first (:arglists (meta #'fast-string-length)))))
;=> {:tag String}

That inspects the metadata on the var, takes its :arglists, and reads the metadata on the x symbol in the argument list. The hint you wrote as ^String is stored as {:tag String} — the same mechanism as the previous section.

TipThe idiomatic workflow

Clojure’s compiler is good at inferring types, and all core functions are already hinted where necessary, so you rarely need this. The recommended approach:

  1. write all your code without type hints
  2. (set! *warn-on-reflection* true)
  3. reevaluate the namespace and add hints one at a time until the warnings go away

Concentrate hints on function arguments and return values — Clojure will usually work out the body for you.

Primitives and arrays

Java primitivesbyte, short, int, long, float, double, boolean, char — are not full objects. They have no pronounceable class name, so Clojure defines aliases: ^byte for the primitive and the plural ^bytes for an array of them.

Arrays of objects are stranger, and finding the class name takes some work:

(defn array-type [klass]
  (.getName (class (make-array klass 0))))

(array-type BigDecimal)
;=> "[Ljava.math.BigDecimal;"

(def bigdec-arr
  ^"[Ljava.math.BigDecimal;"
  (into-array BigDecimal [1.0M]))

You will need this only when writing classes and interfaces in Clojure meant to be consumed by Java code.

Learning outcomes

  • use-java-type-hints: Add type hints to avoid reflection, and measure the difference.
  • attach-and-read-metadata: Attach metadata to a value and explain why it does not change that value.

Concepts

  • type-hints: shows how hints remove reflection, and how they are stored as metadata

Exceptions

Clojure runs on the JVM, so it uses JVM exceptions. Start with a function that can fail:

(defn average [numbers]
  (let [total (apply + numbers)]
    (/ total (count numbers))))

(average [])
ArithmeticException Divide by zero  clojure.lang.Numbers.divide (Numbers.java:156)

Normally you would check for the empty collection, but as an illustration:

(defn safe-average [numbers]
  (let [total (apply + numbers)]
    (try
      (/ total (count numbers))
      (catch ArithmeticException e
        (println "Divided by zero!")
        0))))

(safe-average [])
Divided by zero!
;=> 0

The general form

(try expr* catch-clause* finally-clause?)

Multiple expressions in the try, multiple catch clauses, and an optional finally.

  • the try expressions are evaluated one by one and the value of the last is returned
  • if one throws, the appropriate catch runs based on the exception’s Java class, and its value is returned
  • finally is always executed for side effects, and nothing is ever returned from it

Unlike Java, Clojure has no checked exceptions. Catching and handling are always optional.

Clause order matters

(try
  (print "Attempting division... ")
  (/ 1 0)
  (catch RuntimeException e "Runtime exception!")
  (catch ArithmeticException e "DIVIDE BY ZERO!")
  (catch Throwable e "Unknown exception encountered!")
  (finally
    (println "done.")))
Attempting division... done.
;=> "Runtime exception!"

Not "DIVIDE BY ZERO!", which is the better match. Catch clauses are tried in order and the first possible match is used — and ArithmeticException is a RuntimeException, so the broader clause caught it first.

Arrange catch clauses from most specific to least specific.

And finally runs even when nothing matches:

(try
  (print "Attempting division... ")
  (/ 1 0)
  (finally
    (println "done.")))
Attempting division... done.
ArithmeticException Divide by zero

The exception propagates as normal — but finally still executed.

Throwing

(throw (Exception. "this is an error!"))
Exception this is an error!  user/eval807 (NO_SOURCE_FILE:1)

Note that try is an expression returning a value, which is why safe-average needs no mutable variable. That property is what lets the later functional-programming unit wrap try in a closure and make exception handling a value you can pass around.

Learning outcomes

  • handle-exceptions: Catch and throw JVM exceptions from Clojure.

Concepts

  • exception-handling: covers try, catch, finally and throw, and the clause-ordering rule

Defining functions properly

The previous unit gave defn with one argument list. Here is the rest of it.

(defn total-cost [item-cost number-of-items]
  (* item-cost number-of-items))

which is exactly:

(def total-cost (fn [item-cost number-of-items]
                  (* item-cost number-of-items)))

The docstring and any attribute map attach as metadata on the var — which is how doc finds a docstring at all, and another instance of the first section’s mechanism:

(meta #'total-cost)
;=> {:ns #<Namespace user>, :name total-cost, ...}

Pre- and post-conditions

(defn item-total [price quantity discount-percentage]
  {:pre  [(> price 0) (> quantity 0)]
   :post [(> % 0)]}
  (-> (/ discount-percentage 100)
      (- 1)
      (* price quantity)
      (Math/abs)))

Valid input works:

(item-total 100 2 0)
;=> 200.0
(item-total 100 2 10)
;=> 180.0

Invalid input throws, and says which condition failed:

(item-total 100 -2 10)
AssertionError Assert failed: (> quantity 0)  user/item-total

(item-total 100 2 110)
AssertionError Assert failed: (> % 0)  user/item-total

% refers to the return value in :post. These are assertions rather than a type system — they can be disabled — but they state a function’s contract inside the function.

Multiple arity

(defn function-name
  ([arg1] body-executed-for-one-argument-call)
  ([arg1 arg2] body-executed-for-two-argument-call))

The idiomatic use is a shorter arity that supplies a default and delegates:

(defn total-cost
  ([item-cost number-of-items]
    (* item-cost number-of-items))
  ([item-cost]
    (total-cost item-cost 1)))

That is how Clojure does optional parameters without having optional parameters.

Variadic functions

(defn total-all-numbers [& numbers]
  (apply + numbers))

& collects the rest into a sequence. This is how str, + and every variable-arity function you have used is written — closing a loop from unit 1.

Arities and a variadic tail can combine, and the gaps are real:

(defn many-arities
  ([] 0)
  ([a] 1)
  ([a b c] 3)
  ([a b c & more] "variadic"))

(many-arities)                              ;=> 0
(many-arities "one argument")               ;=> 1
(many-arities "two" "arguments")
ArityException Wrong number of args (2) passed to: user/many-arities
(many-arities "three" "argu-" "ments")      ;=> 3
(many-arities "many" "more" "argu-" "ments");=> "variadic"

There is no two-argument arity, so calling with two throws.

Learning outcomes

  • define-functions-in-full: Define functions with multiple arities, variadic parameters, and pre/post conditions.
  • attach-and-read-metadata: Attach metadata to a value and explain why it does not change that value.

Concepts

  • functions-and-arities: covers defn’s expansion, multiple arity, variadic parameters and assertions
  • metadata: shows docstrings and hints stored as metadata on the var

Recursion and mutual recursion

Direct recursion works, right up until it does not:

(defn count-down [n]
  (when-not (zero? n)
    (when (zero? (rem n 100))
      (println "count-down:" n))
    (count-down (dec n))))

(count-down 100000)
StackOverflowError  clojure.lang.Numbers$LongOps.remainder (Numbers.java:505)

The JVM does not perform tail-call elimination. Each recursive call consumes a stack frame, and 100,000 of them exhaust the stack.

recur, for self-recursion

(defn count-downr [n]
  (when-not (zero? n)
    (if (zero? (rem n 100))
      (println "count-down:" n))
    (recur (dec n))))

recur rebinds and jumps rather than calling, so it uses no additional stack. Same function, no overflow. It only works for self-recursion in tail position.

Mutual recursion

Two functions calling each other cannot both be defined first, and Clojure evaluates top to bottom. declare creates the var without a value so the first can refer to the second:

(declare hat)

(defn cat [n]
  (when-not (zero? n)
    (when (zero? (rem n 100))
      (println "cat:" n))
    (hat (dec n))))

(defn hat [n]
  (when-not (zero? n)
    (if (zero? (rem n 100))
      (println "hat:" n))
    (cat (dec n))))

That compiles — and still blows the stack, because recur cannot help across two functions.

trampoline, for mutual recursion

The fix is to return a function to call next rather than calling directly:

(declare hatt)

(defn catt [n]
  (when-not (zero? n)
    (when (zero? (rem n 100))
      (println "catt:" n))
    (fn [] (hatt (dec n)))))

(defn hatt [n]
  (when-not (zero? n)
    (when (zero? (rem n 100))
      (println "hatt:" n))
    (fn [] (catt (dec n)))))

(trampoline catt 100000)

trampoline calls the function, and while the result is a function, calls that too — in a loop, so the stack never grows.

Key ideas

  • The JVM has no tail-call elimination, so deep recursion overflows the stack.
  • Self-recursive, tail positionrecur.
  • Mutually recursive → return functions and drive them with trampoline.
  • Neither → accept the depth, and know your limit.
  • declare is what makes mutual definition possible at all.

Learning outcomes

  • write-recursive-functions: Write self-recursive and mutually recursive functions in Clojure.

Concepts

  • recursion-rec: covers recur, declare and trampoline against the JVM’s stack constraint

The core higher-order functions

Unit 2’s higher-order functions consumed sequences. These manipulate functions.

Calling with apply

(+ 1 2 3 4 5)
;=> 15
(apply + list-of-expenses)

apply spreads a collection into a function’s arguments — which is how total-all-numbers above worked.

Predicates over collections

(def bools [true true true false false])

(every? true? bools)
;=> false

(some (fn [p] (= "rob" p)) ["kyle" "siva" "rob" "celeste"])
;=> true

some returns the first logical-true result, not merely true, so it can find as well as test.

Building functions from non-functions

(def two (constantly 2))   ; same as (def two (fn [& more] 2))
(two 1)         ;=> 2
(two :a :b :c)  ;=> 2

constantly is for when an API demands a function and you have only a value.

(defn greater? [x y]
  (> x y))
(greater? 10 5)   ;=> true
(greater? 10 20)  ;=> false

(def smaller? (complement greater?))
(smaller? 10 5)   ;=> false
(smaller? 10 20)  ;=> true

complement saves writing (fn [x y] (not (greater? x y))).

Combining functions

(def opp-zero-str (comp str not zero?))

(opp-zero-str 0)  ;=> "false"
(opp-zero-str 1)  ;=> "true"

comp applies right to left: zero?, then not, then str. Read it backwards and the results make sense.

(defn above-threshold? [threshold number]
  (> number threshold))

(filter (fn [x] (above-threshold? 5 x)) [1 2 3 4 5 6 7 8 9])
;=> (6 7 8 9)

(filter (partial above-threshold? 5) [1 2 3 4 5 6 7 8 9])
;=> (6 7 8 9)

partial fixes leading arguments. Same result, no fn.

memoize

(defn slow-calc [n m]
  (Thread/sleep 1000)
  (* n m))

(time (slow-calc 5 7))
;=> 35

(def fast-calc (memoize slow-calc))
(time (fast-calc 5 7))
;=> 35

The second call with the same arguments returns from cache. A one-line change that transforms an expensive pure function — and a trap if the function is not pure, since you will cache a result that should have changed.

Learning outcomes

  • use-core-higher-order-functions: Use the core higher-order functions: every?, some, constantly, complement, comp, partial and memoize.

Concepts

  • higher-order-functions: covers the standard library’s function-manipulating functions

Writing your own higher-order functions

Using them is one thing. Writing them is the shift.

Take a collection of users:

(def users
  [{:username "kyle"
    :firstname "Kyle"
    :lastname "Smith"
    :balance 175.00M            ; Use BigDecimals for money!
    :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"}])

Now a function that returns a sorting function:

(defn sorter-using [ordering-fn]
  (fn [collection]
    (sort-by ordering-fn collection)))

with small functions describing the orderings:

(defn lastname-firstname [user]
  [(user :lastname) (user :firstname)])
(defn balance [user] (user :balance))
(defn username [user] (user :username))

Each ordering is now a one-liner, and sorter-using builds a sorter from any of them.

The caller supplies the behaviour. Compare the alternative: a sort function with a flag for each ordering someone might want. A function with a boolean flag anticipates two cases; a function taking a function anticipates none — it works for cases you never thought of.

This is also the section that sets up the later More on Functional Programming unit, which pushes the same idea until closures start behaving like objects.

Learning outcomes

  • write-higher-order-functions: Write your own function that takes or returns a function.
  • use-core-higher-order-functions: Use the core higher-order functions: every?, some, constantly, complement, comp, partial and memoize.

Concepts

  • higher-order-functions: shows composition of small functions into a configurable sorter

Anonymous functions and callable data

fn, and the shorthand

(map (fn [user] (user :member-since)) users)
;=> ("2009-04-16" "2009-02-01" "2009-03-30")

The #() reader macro is shorter:

(map #(% :member-since) users)
;=> ("2009-04-16" "2009-02-01" "2009-03-30")

Arguments are % or %1, %2, and %& for the rest:

(#(vector %&) 1 2 3 4 5)         ;=> [(1 2 3 4 5)]
(#(vector % %&) 1 2 3 4 5)       ;=> [1 (2 3 4 5)]
(#(vector %1 %2 %&) 1 2 3 4 5)   ;=> [1 2 (3 4 5)]
(#(vector %1 %2 %&) 1 2)         ;=> [1 2 nil]

Note the last: with nothing left over, %& is nil rather than an empty sequence.

NoteWhen not to use the shorthand

It cannot nest — an inner % would be ambiguous. And it hides parameter names, which often document what a function is for.

Use #() for something readable at a glance; use fn once it needs explaining.

Callable data structures

Keywords, maps and vectors can all appear in function position:

(def person {:username "zak" :firstname "Zackary" ...})

(person :username)
;=> "zak"
(:username person)
;=> "zak"

Both directions work, and a keyword takes a default:

(:login person)
;=> nil
(:login person :not-found)
;=> :not-found

Symbols work the same way as map keys:

(def expense {'name "Snow Leopard" 'cost 29.95M})
(expense 'name)   ;=> "Snow Leopard"
('name expense)   ;=> "Snow Leopard"

Why this matters is not brevity at the call site — it is that they can be passed as functions:

(map #(% :member-since) users)
;=> ("2009-04-16" "2009-02-01" "2009-03-30")

(map :member-since users)
;=> ("2009-04-16" "2009-02-01" "2009-03-30")

Same result, and the second needs no anonymous function at all.

Learning outcomes

  • write-anonymous-functions: Write anonymous functions with fn and with the #() reader macro.
  • use-callable-data-structures: Use keywords, maps and vectors in function position.

Concepts

  • functions-and-arities: covers fn and the #() shorthand with its argument syntax
  • callable-data-structures: shows keywords, maps, vectors and symbols used in function position

Lexical and dynamic scope

Two ways a name can get its meaning.

  • Lexical scope — determined by where the name appears in the source. Walk outward through enclosing forms; the first binding you meet is the one. You can work this out by reading, without running anything.
  • Dynamic scope — determined by the call stack at runtime. A name refers to whatever the most recent caller bound it to, which you cannot know from the function alone.

Clojure is lexically scoped by default. let bindings and function parameters are lexical, which is why unit 2 could explain them without this distinction.

Dynamic scope exists and must be asked for explicitly. That explicitness is deliberate: dynamic scope makes a function’s behaviour depend on context invisible at its definition, which is powerful and is exactly why it should be visible in the source.

Learning outcomes

  • distinguish-lexical-and-dynamic-scope: Distinguish lexical scope from dynamic scope and say which Clojure uses where.

Concepts

  • lexical-scope-and-closures: defines lexical scope as determined by the source
  • dynamic-scope-and-bindings: defines dynamic scope as determined by the call stack

Vars and dynamic binding

def creates a var with a root binding:

(def MAX-CONNECTIONS 10)

To rebind it dynamically you must mark it:

(def RABBITMQ-CONNECTION)

(binding [RABBITMQ-CONNECTION (new-connection)]
  ...)
Can't dynamically bind non-dynamic var: user/RABBITMQ-CONNECTION

The error tells you what to do:

(def ^:dynamic RABBITMQ-CONNECTION)

Special variables

The *earmuffs* convention marks a var meant to be rebound:

(def ^:dynamic *db-host* "localhost")

(defn expense-report [start-date end-date]
  (println *db-host*))

(binding [*db-host* "production"]
  (expense-report "2010-01-01" "2010-01-07"))

expense-report never mentions the caller and still sees "production".

Dynamic scope in action

(def ^:dynamic *eval-me* 10)

(defn print-the-var [label]
  (println label *eval-me*))

(print-the-var "A:")
(binding [*eval-me* 20]
  (print-the-var "B:")
  (binding [*eval-me* 30]
    (print-the-var "C:"))
  (print-the-var "D:"))
(print-the-var "E:")

prints:

A: 10
B: 20
C: 30
D: 20
E: 10

Bindings nest and unwind. D: sees 20 again once the inner binding exits, and E: is back to the root.

Rebinding a function

Because a function is just a value in a var, you can rebind that too:

(defn ^:dynamic twice [x]
  (println "original function")
  (* 2 x))

(defn call-twice [y]
  (twice y))

(defn with-log [function-to-call log-statement]
  (fn [& args]
    (println log-statement)
    (apply function-to-call args)))

(call-twice 10)
(binding [twice (with-log twice "Calling the twice function")]
  (call-twice 20))
(call-twice 30)

call-twice is untouched, and gains logging for the duration of the binding. That is aspect-oriented logging with no framework.

Thread-local state

A var’s root binding is visible to all threads unless a binding form overrides it in a particular thread. If a thread overrides it, that binding is not visible to any other thread. Nested bindings exist until the thread exits.

ImportantLaziness and special variables

This one is genuinely baffling until you have seen it.

(def ^:dynamic *factor* 10)
(defn multiply [x]
  (* x *factor*))

(map multiply [1 2 3 4 5])
;=> (10 20 30 40 50)

(binding [*factor* 20]
  (map multiply [1 2 3 4 5]))
;=> (10 20 30 40 50)

The second call sets *factor* to 20 and gets the same answer.

map returns a lazy sequence, not realised until needed — which happens when the REPL prints it, by which time execution has left the binding form and *factor* has reverted to its root.

Force realisation inside the binding:

(binding [*factor* 20]
  (doall (map multiply [1 2 3 4 5])))
;=> (20 40 60 80 100)

doall forces a lazy sequence. Be careful with very large ones — the usual alternative is to re-establish the binding inside the function generating the elements.

let versus binding

They look alike and are not:

(binding [*factor* 20]
  (println *factor*)
  (doall (map multiply [1 2 3 4 5])))
;; prints 20, returns (20 40 60 80 100)

(let [*factor* 20]
  (println *factor*)
  (doall (map multiply [1 2 3 4 5])))
;; prints 20, returns (10 20 30 40 50)

let creates a new lexical binding visible only in its body’s text. multiply refers to the var, which let never touched — so it still sees 10.

binding rebinds the existing var, so every function called during the dynamic extent sees the new value, including ones that know nothing about the caller.

let also scopes functions locally:

(defn upcased-names [names]
  (let [up-case (fn [name] (.toUpperCase name))]
    (map up-case names)))

(upcased-names ["foo" "bar" "baz"])
;=> ("FOO" "BAR" "BAZ")

Learning outcomes

  • use-vars-and-dynamic-binding: Create dynamic vars and rebind them per thread with binding.
  • distinguish-lexical-and-dynamic-scope: Distinguish lexical scope from dynamic scope and say which Clojure uses where.

Concepts

  • dynamic-scope-and-bindings: covers root bindings, ^:dynamic, thread-locality and the laziness trap
  • lexical-scope-and-closures: contrasts let’s lexical binding with binding’s dynamic rebinding

Lexical closures

The most important section in the unit for what comes later.

(defn create-scaler [scale]
  (fn [x]
    (* x scale)))

create-scaler returns a function. That inner function uses scale — which is neither its own parameter nor bound in its body. scale is a free variable, and the inner function closes over it.

(def percent-scaler (create-scaler 100))

(percent-scaler 0.59)
;=> 59.0

create-scaler returned long ago. Its scale binding would normally be gone — and percent-scaler still has it.

Three consequences worth stating:

  • the captured binding outlives the form that created it

    Which is why a closure can carry state with no mutable variable anywhere.

  • each closure gets its own capture

    (create-scaler 100) and (create-scaler 2) produce independent functions that do not interfere.

  • the function is configured at creation rather than parameterised at every call

    percent-scaler takes one argument, not two. The scale was decided once.

This looks small. It is the mechanism behind the later unit’s claim that closures and objects are the same idea from opposite directions — a claim that only means something once you have seen captured state persist.

Learning outcomes

  • write-lexical-closures: Write a closure and identify its free variables.
  • distinguish-lexical-and-dynamic-scope: Distinguish lexical scope from dynamic scope and say which Clojure uses where.

Concepts

  • lexical-scope-and-closures: defines closures through free variables and captured bindings

Namespaces

A namespace maps symbols to vars. Two purposes: preventing name collisions, and grouping related code.

The ns macro

(ns name & references)
(ns org.currylogic.damages.calculators)

(defn highest-expense-during [start-date end-date]
  ...)

Loading other code

The older style uses use, which pulls names in directly:

(ns org.currylogic.damages.http.expenses)
(use 'clojure.data.json)
(use 'clojure.xml)

(declare load-totals)

(defn import-transactions-xml-from-bank [url]
  (let [xml-document (parse url)]
    ...))

(defn totals-by-day [start-date end-date]
  (let [expenses-by-day (load-totals start-date end-date)]
    (json-str expenses-by-day)))

parse and json-str are now unqualified — convenient, and you cannot tell where they came from.

require with an alias is better:

(ns org.currylogic.damages.http.expenses)
(require '(clojure.data [json :as json-lib]))
(require '(clojure [xml :as xml-core]))

(declare load-totals)

(defn import-transactions-xml-from-bank [url]
  (let [xml-document (xml-core/parse url)]
    ...))

(defn totals-by-day [start-date end-date]
  (let [expenses-by-day (load-totals start-date end-date)]
    (json-lib/json-str expenses-by-day)))

Now xml-core/parse says where it comes from. Prefer this.

:import is separate, because Java classes are not vars.

Private functions are marked with metadata — the first section’s mechanism again — keeping them out of the namespace’s public interface.

At runtime, the programmatic functions create, switch, list and inspect namespaces. This is not exotic: switching namespace to test a function in its own context is normal mid-session, and it is what makes REPL-driven development work.

Learning outcomes

  • organise-code-with-namespaces: Organise code into namespaces with the ns macro and manipulate them at runtime.
  • use-vars-and-dynamic-binding: Create dynamic vars and rebind them per thread with binding.

Concepts

  • namespaces: covers the ns macro, use versus require, privacy, and runtime manipulation
  • metadata: shows privacy marked as metadata on the var

Destructuring

Instead of binding a structure and then digging into it:

(defn describe-salary [person]
  (let [first (:first-name person)
        last (:last-name person)
        annual (:salary person)]
    (println first last "earns" annual)))

describe its shape in the binding position:

(defn describe-salary-2 [{first :first-name
                          last :last-name
                          annual :salary}]
  (println first last "earns" annual))

The function’s interface is now visible in its first line.

Vector bindings

(defn print-amounts [[amount-1 amount-2]]
  (println "amounts are:" amount-1 "and" amount-2))

(print-amounts [10.95 31.45])

& collects the rest, :as names the whole:

(defn print-amounts-multiple [[amount-1 amount-2 & remaining]]
  (println "Amounts are:" amount-1 "," amount-2 "and" remaining))

(print-amounts-multiple [10.95 31.45 22.36 2.95])
Amounts are: 10.95 , 31.45 and (22.36 2.95)
(defn print-all-amounts [[amount-1 amount-2 & remaining :as all]]
  (println "Amounts are:" amount-1 "," amount-2 "and" remaining)
  (println "Also, all the amounts are:" all))

(print-all-amounts [10.95 31.45 22.36 2.95])
Amounts are: 10.95 , 31.45 and (22.36 2.95)
Also, all the amounts are: [10.95 31.45 22.36 2.95]

And patterns nest, with _ for parts you do not want:

(defn print-first-category [[[category amount] & _]]
  (println "First category was:" category)
  (println "First amount was:" amount))

(def expenses [[:books 49.95] [:coffee 4.95] [:caltrain 2.25]])
(print-first-category expenses)
First category was: :books
First amount was: 49.95

Map bindings

(def a-user {:first-name "pascal"
             :last-name "dylan"
             :salary 85000
             :bonus-percentage 20})

Bind by key, with :or supplying defaults for keys that may be absent:

(defn describe-salary-3 [{first :first-name
                          last :last-name
                          annual :salary
                          bonus :bonus-percentage
                          :or {bonus 5}}]
  (println first last "earns" annual "with a" bonus "percent bonus"))

Called with a-user it reports 20; called without a :bonus-percentage it reports 5 rather than nil. That is how optional arguments are normally done.

:keys is the shorthand for the common case where the local name matches the keyword, and :as names the whole map.

Where it works: anywhere a binding appears — let, function parameters, doseq, for, and the binding forms of most macros.

Learning outcomes

  • destructure-bindings: Destructure vectors and maps inside any binding form.

Concepts

  • destructuring: covers sequential and associative patterns, &, :as, :or and nesting

Reader literals

A reader literal is a #tag followed by a form, handled by the reader — before evaluation — via a function registered for that tag. Built-in examples are #inst for instants and #uuid for UUIDs.

You can register your own, and the chapter works a custom UUID-style example. The shape that suits it: a value with a natural textual form and a richer runtime representation.

What this extends is the data format, not the syntax. You are not adding grammar; you are saying how an existing shape — tag plus form — becomes a value. That restraint is why tagged literals travel safely between programs: a reader that does not know your tag can be told what to do with unknown tags rather than failing to parse.

NoteThe theme this chapter has been circling

Three things here have now shown the same idea:

  • metadata attaches to data without changing it
  • defn is data the reader shapes into a definition, with docstrings stored as metadata
  • reader literals hook the reader itself

All three depend on Clojure code being data. The macro units make that the entire subject.

Learning outcomes

  • explain-reader-literals: Explain what a reader literal is and define one of your own.

Concepts

  • reader-literals: explains tagged literals as a reader-level extension point

What you can now build

That is the machinery. Two threads from this unit run through the rest of the module.

Closures, from the scope sections, are what the later More on Functional Programming unit builds an entire object system out of.

Code as data — metadata, defn’s expansion, reader literals — is what the two macro units are about.

Learning outcomes

  • attach-and-read-metadata: Attach metadata to a value and explain why it does not change that value.
  • use-java-type-hints: Add type hints to avoid reflection, and measure the difference.
  • handle-exceptions: Catch and throw JVM exceptions from Clojure.
  • define-functions-in-full: Define functions with multiple arities, variadic parameters, and pre/post conditions.
  • write-recursive-functions: Write self-recursive and mutually recursive functions in Clojure.
  • use-core-higher-order-functions: Use the core higher-order functions: every?, some, constantly, complement, comp, partial and memoize.
  • write-higher-order-functions: Write your own function that takes or returns a function.
  • write-anonymous-functions: Write anonymous functions with fn and with the #() reader macro.
  • use-callable-data-structures: Use keywords, maps and vectors in function position.
  • distinguish-lexical-and-dynamic-scope: Distinguish lexical scope from dynamic scope and say which Clojure uses where.
  • use-vars-and-dynamic-binding: Create dynamic vars and rebind them per thread with binding.
  • write-lexical-closures: Write a closure and identify its free variables.
  • organise-code-with-namespaces: Organise code into namespaces with the ns macro and manipulate them at runtime.
  • destructure-bindings: Destructure vectors and maps inside any binding form.
  • explain-reader-literals: Explain what a reader literal is and define one of your own.

Concepts

  • higher-order-functions: collects the function-manipulating vocabulary
  • destructuring: collects the binding-form patterns
  • functions-and-arities: collects the full function-definition vocabulary

Conclusion

  • Metadata adds identity to a value without changing the value.

    Two maps with different metadata still compare equal. The mechanism turns up again as type hints, as docstrings on vars, and as privacy markers — one idea used four ways.

  • Type hints remove reflection, and the difference is measurable.

    45.751 msecs to 5.788 for the same work. The workflow is to write without hints, turn on *warn-on-reflection*, and add them one at a time until the warnings stop.

  • The JVM has no tail-call elimination, so recursion needs help.

    recur for self-recursion in tail position; trampoline and returned functions for mutual recursion; declare to make mutual definition possible at all.

  • Higher-order functions are worth writing, not just using.

    comp and partial build functions without fn. A function taking a function anticipates cases its author never thought of, where a boolean flag anticipates exactly two.

  • Lexical scope is the default; dynamic scope must be asked for.

    let binds lexically and binding rebinds a var dynamically — and the difference bites when a lazy sequence is realised after the binding has unwound.

  • A closure outlives the scope that created it.

    create-scaler returns, and its scale lives on inside the function it produced. That single fact is what the later unit turns into an object system.

Where next

The next unit, State and the Concurrent World, takes a different thread — the value/identity distinction from unit 1 — and makes it concrete: refs with software transactional memory, agents, atoms and vars, one unified access model across all four, and futures and promises for parallelism without shared state.