Building Blocks of Clojure

Clojure

2026-08-20 10:00

Where we are

Unit 2 moved fast

Collections, defn, let, conditionals, sequence functions, threading macros.

It did not stop to explain the machinery.

This unit stops.

The reference chapter

  • Metadata and type hints
  • Exceptions
  • Functions in full
  • Scope — lexical and dynamic
  • Namespaces, destructuring, reader literals

Work through it with a REPL open. Expect to come back to it.

Two things that recur later

Closures → the later unit builds an object system from them.

Code as data → the macro units depend on it.

Metadata

Data about data

Tag a map, list or vector with other data

without changing the value of the tagged data.

Why

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.

Attaching it

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

Or with the reader macro, at read time:

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

A silent trap

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

This tags the list starting with hash-map — the code.

Not the map the call produces.

The metadata becomes invisible at runtime. Nothing errors.

It does not affect equality

(def trusted {:command "delete-table" :subject "users"})

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

And it is carried forward

(def still-untrusted (assoc untrusted :complete? false))

(meta still-untrusted)
;=> {:safe false, :io true}

Preserving the identity semantics.

Type hints

Why reflection happens

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.

Measure it

(set! *warn-on-reflection* 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"
(defn fast-string-length [^String x] (.length x))

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

Eight times faster

And no reflection warning.

The hint really is metadata

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

^String is stored as {:tag String}.

Same mechanism as the previous section.

The idiomatic workflow

  1. write everything without hints
  2. (set! *warn-on-reflection* true)
  3. add hints one at a time until the warnings stop

Concentrate on arguments and return values — Clojure infers the body.

Primitives and arrays

^byte for the primitive · ^bytes for the array

Object arrays need magic:

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

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

Exceptions

A function that can fail

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

(average [])
ArithmeticException Divide by zero

Catching it

(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?)
  • try expressions evaluate in order; the last one’s value is returned
  • on a throw, the matching catch runs and its value is returned
  • finally always runs — and never returns anything

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 the better match — the first matching clause wins, and ArithmeticException is a RuntimeException.

So

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.

Defining functions properly

defn is def plus fn

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

The docstring attaches as metadata on the var — which is how doc finds it.

Pre- and post-conditions

(defn item-total [price quantity discount-percentage]
  {:pre  [(> price 0) (> quantity 0)]
   :post [(> % 0)]}
  ...)
(item-total 100 2 10)   ;=> 180.0

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

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

% is the return value in :post.

Multiple arity

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

How Clojure does optional parameters without having optional parameters.

Variadic

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

& collects the rest. This is how str and + are written.

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")              ;=> 1
(many-arities "two" "arguments")
ArityException Wrong number of args (2)
(many-arities "three" "argu-" "ments")  ;=> 3

Recursion

Direct recursion works, 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

The JVM has no tail-call elimination.

recur

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

Rebinds and jumps rather than calling. No stack growth.

Only works for self-recursion in tail position.

Mutual recursion needs declare

(declare hat)

(defn cat [n]
  (when-not (zero? n)
    ...
    (hat (dec n))))

(defn hat [n]
  (when-not (zero? n)
    ...
    (cat (dec n))))

Compiles — and still blows the stack. recur cannot cross two functions.

trampoline

Return a function to call next:

(defn catt [n]
  (when-not (zero? n)
    ...
    (fn [] (hatt (dec n)))))

(defn hatt [n]
  (when-not (zero? n)
    ...
    (fn [] (catt (dec n)))))

(trampoline catt 100000)

trampoline calls in a loop, so the stack never grows.

Which one

Self-recursive, tail position → recur

Mutually recursive → trampoline

Neither → accept the depth, and know your limit

Core higher-order functions

Predicates over collections

(every? true? [true true true false false])
;=> false

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

some returns the first logical-true result — so it can find, not just test.

Building functions from values

(def two (constantly 2))
(two 1)         ;=> 2
(two :a :b :c)  ;=> 2
(def smaller? (complement greater?))
(smaller? 10 5)   ;=> false
(smaller? 10 20)  ;=> true

comp — right to left

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

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

zero?, then not, then str. Read it backwards.

partial

(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)

Same result. No fn.

memoize

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

(def fast-calc (memoize slow-calc))

A one-line change for an expensive pure function.

And a trap if it is not pure.

Writing your own

The data

(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 that returns a function

(defn sorter-using [ordering-fn]
  (fn [collection]
    (sort-by ordering-fn collection)))
(defn lastname-firstname [user]
  [(user :lastname) (user :firstname)])
(defn balance [user] (user :balance))
(defn username [user] (user :username))

Who supplies the behaviour

A function with a boolean flag anticipates two cases.

A function taking a function anticipates none —

it works for cases you never thought of.

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")

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

The argument syntax

(#(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]

With nothing left over, %& is nil, not an empty sequence.

When not to use it

It cannot nest — an inner % would be ambiguous.

It hides parameter names, which often document intent.

#() for a glance. fn once it needs explaining.

Callable data structures

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

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

Symbols work as keys too:

(expense 'name)   ;=> "Snow Leopard"
('name expense)   ;=> "Snow Leopard"

Why it matters

(map #(% :member-since) users)
(map :member-since users)

Same result. No anonymous function at all.

Scope

Two ways a name gets meaning

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.

Clojure’s default

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.

Vars and binding

Marking a var dynamic

(def RABBITMQ-CONNECTION)

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

Special variables

(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".

Bindings nest and unwind

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

(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:")
A: 10   B: 20   C: 30   D: 20   E: 10

Rebinding a function

(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)))

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

Aspect-oriented logging. No framework.

Thread-local

A var’s root binding is visible to all threads.

A binding override is visible to none but the current one.

The laziness trap

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

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

Twenty was set. Ten was used.

Why

map is lazy. The sequence is realised when the REPL prints it —

by which time execution has left the binding.

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

let versus binding

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

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

let makes a new lexical binding. multiply refers to the var, untouched.

Lexical closures

A function that returns a function

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

scale is neither the inner function’s parameter nor bound in its body.

It is a free variable, and the function closes over it.

The capture outlives its scope

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

(percent-scaler 0.59)
;=> 59.0

create-scaler returned long ago. Its scale is still there.

Three consequences

  • the captured binding outlives the form that created it
  • each closure gets its own capture
  • the function is configured at creation, not parameterised at every call

This is what the later unit turns into an object system.

Namespaces

use pulls names in

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

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

parse is unqualified — and you cannot tell where it 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]))

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

Now the name says where it comes from. Prefer this.

And at runtime

Namespaces can be created, switched and inspected while the program runs.

Not exotic — it is what makes REPL-driven development work.

Destructuring

Before

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

After

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

The interface is now visible in the first line.

Vector patterns

(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]]
  ...)
Also, all the amounts are: [10.95 31.45 22.36 2.95]

They nest

(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 patterns with defaults

(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"))

:or is how optional arguments are normally done.

Where it works

Anywhere a binding appears.

let · function parameters · doseq · for · most macros

Reader literals

Tagged literals

#inst · #uuid

A tag plus a form, handled by the reader — before evaluation —

via a function registered for that tag.

What it extends

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.

The theme this chapter circled

  • metadata attaches to data without changing it
  • defn is data the reader shapes into a definition
  • reader literals hook the reader itself

All three depend on Clojure code being data.

The macro units make that the entire subject.

Summary

The six things to carry away

  • Metadata adds identity without changing the value — and reappears as hints, docstrings and privacy markers.
  • Type hints remove reflection: 45.751 msecs became 5.788.
  • No tail-call elimination: recur for self, trampoline for mutual.
  • A function taking a function anticipates cases its author never thought of.
  • Lexical by default; binding is dynamic, thread-local, and bites with laziness.
  • A closure outlives the scope that created it.

Where next

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.