Lecture notes — Clojure Elements: Data Structures and Functions

Published

2026-08-20 00:00

Keywords

ver. 1.0.0

← Clojure Elements: Data Structures and Functions

Where we are

In Introducing Clojure you learned to read Clojure: prefix notation, what parentheses are for, the value/identity distinction, and enough Java interop to call Math/abs.

Reading is not writing. This unit closes that gap, and it is the longest and most reference-like unit in the module because it has to hand you the whole working vocabulary at once.

Almost everything here is a form to type rather than read. The REPL is not an accessory to learning Clojure — it is where the learning happens, which is why we start there.

What you will be able to do

  1. work-at-the-repl — Evaluate expressions at the REPL and use its magic variables and documentation tools.
  2. explain-prefix-notation-consequences — Explain why prefix notation makes code easy to generate and manipulate.
  3. use-clojure-scalars — Work with Clojure’s scalar types: nil, booleans, characters, strings and the number tower.
  4. distinguish-symbols-and-keywords — Distinguish a symbol from a keyword and say when to reach for each.
  5. choose-a-collection — Choose between a list, a vector and a map, and use each one’s core operations.
  6. use-the-sequence-abstraction — Use the ISeq abstraction that unifies every Clojure collection.
  7. structure-a-program — Define functions, bind locals with let, and sequence side effects with do.
  8. choose-a-conditional — Choose the right conditional form from if, if-not, cond, when and when-not.
  9. iterate-functionally — Replace imperative loops with recur, the sequence functions, and list comprehension.
  10. use-threading-macros — Flatten nested calls into a readable pipeline with the threading macros.

What we will cover

  • REPL — interactive evaluation, magic variables, and documentation lookup.
  • Prefix notation — the uniform syntax, and why it makes code easy to generate.
  • Numbers and ratios — the numeric tower, arbitrary precision, and contagiousness.
  • Symbols and keywords — identifiers that resolve, versus values that evaluate to themselves.
  • Persistent collections — lists, vectors and maps, and what each is good at.
  • ISeqfirst, rest and cons across every collection.
  • The let form — lexical local bindings.
  • loop/recur — stack-safe recursion in place of iteration.
  • Higher-order sequence functionsmap, filter, remove, reduce, and for.
  • Threading macros — pipelining nested operations into a readable order.

From reading to writing

The four parts of this unit:

  • Coding at the REPL — the interactive loop, plus the documentation tools that let you answer your own questions
  • Data structures — the scalars, then lists, vectors, maps, and the abstraction that unifies them
  • Program structuredefn, let, do, and reader macros
  • Program flow — conditionals, logical functions, functional iteration, and threading macros

By the end you will have the vocabulary to write small to medium programs, which is what every later unit assumes.

Learning outcomes

  • work-at-the-repl: Evaluate expressions at the REPL and use its magic variables and documentation tools.
  • explain-prefix-notation-consequences: Explain why prefix notation makes code easy to generate and manipulate.

Coding at the REPL

Start with tradition:

user> (println "Hello, world!")
Hello, world!
=> nil

Two lines, and the difference between them is the REPL in miniature. println is unusual for Clojure because it is a side-effecting function: it prints a string to standard out and then returns nil.

  • Hello, world! was printed during the REPL’s evaluation phase
  • => nil was printed during its print phase

Normally you want pure functions that only return a result and do not modify the world.

From here on the book omits the user> prompt and begins result lines with ;=>. It is a convention designed to make code easy to copy-paste between files and REPLs.

The magic variables

Four of them, and they save real typing:

  • *1, *2, *3 — the last, second-last and third-last successfully read forms
  • *e — the last error

Each time a form evaluates successfully its value goes into *1, the old *1 moves to *2, and the old *2 moves to *3.

"expression 1"
;=> "expression 1"
"expression 2"
;=> "expression 2"
*1
;=> "expression 2"
*3
;=> "expression 1"

Multiple forms on one line each shift the chain:

"a" "b" "c"
;=> "a"
;=> "b"
;=> "c"
*3
;=> "a"
NoteA subtlety worth catching

(def a-str *1) captures the value that was in *1 at that moment. Evaluate something else and *1 moves on, but a-str still holds what it captured.

On an error the numbered variables stay where they were, and the error binds to *e:

( ) )
;=> ( )
RuntimeException Unmatched delimiter: )
*1
;=> ( )
*e
;=> #<ReaderException ... Unmatched delimiter: )>

Answering your own questions

Three functions matter more than they look, because they turn the REPL from a place that evaluates into a place you can explore:

  • doc — prints a function’s docstring and argument spec
  • find-doc — searches docstrings by pattern, for when you do not know the name
  • apropos — searches names

doc output for +, showing the argument spec alongside the docstring.

Learning outcomes

  • work-at-the-repl: Evaluate expressions at the REPL and use its magic variables and documentation tools.

Concepts

  • repl: introduces interactive evaluation, the magic variables, and the documentation tools

Syntax revisited

The previous unit explained prefix notation and deferred the deeper reason. This is that reason.

You already write prefix notation

Prefix notation feels alien for arithmetic — (+ 1 2) rather than 1 + 2. But consider calling a function in Ruby:

add(1, 2)

Look closely and this is also prefix notation: the function name appears first, followed by arguments. Clojure moves the parenthesis and drops the comma, since whitespace is enough to delimit arguments:

(add 1 2)

In most languages, mathematical functions are special cases built into the language as operators so that maths can be written infix. Clojure avoids the special case by not having operators at all — the maths functions are ordinary functions, and all functions work the same way.

What the regularity buys

By avoiding special cases and relying on the same notation everywhere, Clojure gets the advantages of having no syntax. The main one:

it makes it easy to generate and manipulate code.

The book’s example is cond:

(def x 1)
;=> #'user/x

(cond
  (> x 0) "greater!"
  (= x 0) "zero!"
  (< x 0) "lesser!")
;=> "greater!"

This is a nested list containing an even number of expressions in pairs. The first element of each pair is a test; the second is what gets evaluated and returned if the test succeeds.

Generating such a list is easy — compare it with generating a case statement in Java. This is your first concrete taste of why homoiconicity matters, and the two macro units later in this module cash it in completely.

Whitespace and comments

Clojure does not need commas to delimit elements. It treats them as whitespace and ignores them, so all of these are equivalent:

(+ 1 2 3 4 5)
;=> 15
(+ 1, 2, 3, 4, 5)
;=> 15
(+ 1,,,,2,3 4,,5)
;=> 15

The printer does use commas when echoing a map, purely for readability:

(def a-map {:a 1 :b 2 :c 3})
user> a-map
{:a 1, :c 3, :b 2}

The key order differs because hash maps are not ordered — it makes no difference to the map, only to how it prints.

Comments use semicolons. By convention: one after code on the same line, two for a whole commented line, three for a block. For multiple lines there is the comment macro, which ignores the forms passed in and returns nil:

(comment
  (defn this-is-not-working [x y]
    (+ x y)))
;=> nil

Learning outcomes

  • explain-prefix-notation-consequences: Explain why prefix notation makes code easy to generate and manipulate.

Concepts

  • prefix-notation: explains the uniform prefix syntax and the code-generation advantage it produces

Scalars and the number tower

nil, truth and falsehood

nil is equivalent to null in Java and nil in Ruby — it means “nothing”. Calling a function on nil may lead to a NullPointerException, though core Clojure functions try to do something reasonable.

The truthiness rule is short enough to memorise:

Everything other than false and nil is considered true.

There is an explicit true when you need it.

Characters and strings

Both are Java’s. Characters are unsigned 16-bit UTF-16 code points, written with the backslash reader macro: \a, \g. Strings are Java strings in double quotes — single quotes are a reader macro meaning something else entirely.

Because they are Java strings, the Java String API is directly useful:

(.contains "clojure-in-action" "-")
(.endsWith "program.clj" ".clj")

Both return true. Note the leading periods — that is the interop syntax from the previous unit.

Numbers

Most of the time you are using 64-bit integers (Java long) or 64-bit floats (Java double). When you need more range there are big integers and big decimals. And Clojure adds a less common type: the ratio, created when two integers divide without reducing further — (/ 4 9) gives 4/9.

Type Syntax examples Contagiousness
Integer 42, 0x2a, 052, 2r101010, -42 0 (lowest)
Big integer 42N, 0x2aN, 052N 1
Ratio 1/3, -2/4 2
Big decimal 2.78M, 278e-2M 3
Floating point 2.78, 278e-2 4 (highest)

Contagiousness decides mixed results: the most contagious type infects the result. Watch it climb:

(+ 1 1N)
;=> 2N
(+ 1 1N 1/2)
;=> 5/2
(+ 1 1N 1/2 0.5M)
;=> 3.0M
(+ 1 1N 1/2 0.5M 0.5)
;=> 3.5
ImportantOverflow, and the quoted operators

Adding, subtracting and multiplying integers can overflow — dividing cannot, since out-of-range division produces a ratio. Normally overflow throws:

user> (inc 9223372036854775807)
ArithmeticException integer overflow

The variants +', -', *', inc' and dec' — spelled identically but with a trailing single quote — autopromote to big integers instead:

user> (inc' 9223372036854775807)
;=> 9223372036854775808N

Learning outcomes

  • use-clojure-scalars: Work with Clojure’s scalar types: nil, booleans, characters, strings and the number tower.

Concepts

  • ratios-and-numbers: details the numeric types, arbitrary precision, exact ratios, and overflow behaviour

Symbols and keywords

Symbols are the identifiers in a Clojure program — the names that signify values. In (+ 1 2), the + is a symbol signifying the addition function.

Because Clojure separates reading from evaluating, a symbol has two distinct aspects: its existence in the program data structure after reading, and the value it resolves to. Symbols by themselves are just names with an optional namespace; when an expression is evaluated they are replaced with the value they signify.

The syntax, which is easier to recognise than to state. A symbol is any run of alphanumerics or the characters *!_?$%&=<>, with restrictions: no leading number; no number as second character after -, + or . (so they cannot be confused with number literals); and at most one /, in the middle, separating namespace from name.

  • valid: foo, foo/bar, ->Bar, -foo, foo?, foo-bar, foo+bar
  • invalid: /bar, /foo, +1foo

Quoting makes a symbol data

arglebarg
CompilerException ... Unable to resolve symbol: arglebarg in this context.

'arglebarg
;=> arglebarg

The first fails because arglebarg is not bound to anything. The second evaluates the symbol itself as a value. The quote tells the reader that the next form is literal data, not code to evaluate later.

Keywords are built for that job

In practice you will almost never quote a symbol to use it as data, because Clojure has a type specifically for the purpose.

A keyword is sort of like an autoquoted symbol: keywords never reference some other value and always evaluate to themselves.

Keyword syntax is almost symbol syntax with a leading colon: :foo, :foo/bar, :->foo, :+. You will use them constantly — typically as keys in hash maps and as enumerated values.

Converting between them

(keyword "foo")
;=> :foo
(symbol "foo" "bar")
;=> foo/bar
(name :foo/bar)
;=> "bar"
(namespace :foo)
;=> nil
(name "baz")
;=> "baz"

Two details from those examples: namespace returns nil when there is no namespace part, and name returns strings unchanged.

Learning outcomes

  • distinguish-symbols-and-keywords: Distinguish a symbol from a keyword and say when to reach for each.

Concepts

  • symbols-and-keywords: explains identifier resolution for symbols versus self-evaluating keywords

Lists, vectors and maps

Three collections. They differ in what they are good at, not merely in syntax.

Lists

Singly linked, so it is easy to go from first to last and impossible to go backward. Items can only be added or removed at the front. That constraint has a payoff: multiple lists can share the same tails, which makes lists the simplest possible immutable data structure.

(list 1 2 3 4 5)
;=> (1 2 3 4 5)

(conj (list 1 2 3 4 5) 6)
;=> (6 1 2 3 4 5)
(conj (list 1 2 3) 4 5 6)
;=> (6 5 4 1 2 3)

Note where conj puts things. Conjoining several at once gives the same result as conjoining one at a time.

Treat a list as a stack with peek and pop:

(peek (list 1 2 3))
;=> 1
(pop (list 1 2 3))
;=> (2 3)
(peek (list))
;=> nil
(pop (list))
IllegalStateException Can't pop empty list

count is constant time.

ImportantLists are special

The compiler reads lists as code. So this fails:

(def three-numbers (1 2 3))

It tries to call 1 as a function. Quote it to say “this is data”:

(def three-numbers '(1 2 3))
;=> #'user/three-numbers

Every Clojure beginner hits this once.

Vectors

Indexed, with [] literal syntax.

(vector 10 20 30 40 50)
;=> [10 20 30 40 50]
(def the-vector [10 20 30 40 50])

(get the-vector 2)
;=> 30
(nth the-vector 2)
;=> 30

get and nth agree until you go out of range, and then they differ — which is the thing to remember:

(get the-vector 10)
;=> nil
(nth the-vector 10)
IndexOutOfBoundsException

assoc returns a new vector with one index changed:

(assoc the-vector 2 25)
;=> [10 20 25 40 50]

Maps

Literal {}, or the hash-map function:

(def the-map {:a 1 :b 2 :c 3})
(hash-map :a 1 :b 2 :c 3)
;=> {:a 1, :c 3, :b 2}

Both the map and the keyword are callable, so there are two lookup styles, plus a default:

(the-map :b)
;=> 2
(:b the-map)
;=> 2
(:z the-map 26)
;=> 26

assoc adds, dissoc removes — both returning new maps:

(def updated-map (assoc the-map :d 4))
;=> {:d 4, :a 1, :b 2, :c 3}
(dissoc updated-map :a)
;=> {:b 2, :c 3, :d 4}

Nested data

Real data nests, so three functions take a path rather than a key:

(assoc-in users [:kyle :summary :average :monthly] 3000)
;=> {:kyle {:date-joined "2009-01-01",
;=>         :summary {:average {:monthly 3000, :yearly 12000}}}}

(get-in users [:kyle :summary :average :monthly])
;=> 1000

(update-in users [:kyle :summary :average :monthly] + 500)
;=> {:kyle {... :monthly 1500 ...}}

Their general forms:

(assoc-in map [key & more-keys] value)
(update-in map [key & more-keys] update-function & args)

If a nested map does not exist along the way, assoc-in creates it. And update-in takes a function rather than a value, which is the difference between the two.

Learning outcomes

  • choose-a-collection: Choose between a list, a vector and a map, and use each one’s core operations.

Concepts

  • persistent-collections: covers immutable lists, indexed vectors and associative maps, and their operations

The sequence abstraction

This section is short and structurally the most important in the unit.

The ISeq interface provides three functions — first, rest and cons — and every collection supports them.

(first (list 1 2 3))
;=> 1
(rest (list 1 2 3))
;=> (2 3)

(first [1 2 3])
;=> 1
(rest [1 2 3])
;=> (2 3)

Two results are worth pausing on:

(first {:a 1 :b 2})
;=> [:b 2]
(rest {:a 1 :b 2})
;=> ([:a 1])

A map, viewed as a sequence, is a sequence of key/value pairs — so first gives you a pair, not a key.

(first [])
;=> nil
(rest [])
;=> ()

rest of an empty collection is (), not nil. That is why you can keep calling rest without a nil check at every step.

And cons prepends:

(cons 1 [2 3 4 5])
;=> (1 2 3 4 5)

Every collection presenting the same interface is what makes the rest of the unit work. When we reach map, filter, remove and reduce, none of them needs to know whether it was handed a list, a vector or a map. One abstraction, defined here, carries the whole module.

Learning outcomes

  • use-the-sequence-abstraction: Use the ISeq abstraction that unifies every Clojure collection.
  • choose-a-collection: Choose between a list, a vector and a map, and use each one’s core operations.

Concepts

  • iseq: introduces the sequence interface — first, rest, cons — shared across all collections

Structuring a program

Functions

(defn addition-function [x y]
  (+ x y))

And it is worth seeing what defn actually is — def combined with fn:

(def addition-function
  (fn [x y]
    (+ x y)))

Once that equivalence is visible, a lot follows. A function is a value bound to a name, exactly as unit 1 promised; fn produces one without naming it. Functions may also have variable arity, with different bodies for different argument counts.

The let form

let introduces lexical local bindings. The motivation is legibility, and the book’s example shows it. Written as one expression:

(defn average-pets []
  (/ (apply + (map :number-pets (vals users))) (count users)))

Written with let, each intermediate step gets a name:

(defn average-pets []
  (let [user-data (vals users)
        pet-counts (map :number-pets user-data)
        total (apply + pet-counts)]
    (/ total (count users))))

Not shorter. Legible, which is the point.

Bindings are sequential, so a later one may refer to an earlier one:

(let [x 1
      y (+ x 1)
      z (+ y 1)]
  z)
;=> 3
TipThe underscore convention

Bind to _ when you need the binding to happen but do not need the value — typically a side-effecting call inside a let. It signals intent to the reader instead of inventing a name nobody uses.

Side effects with do

In a world without state and side effects, a function whose body is several expressions would be equivalent to one containing only the last:

(defn do-many-things []
  (do-first-thing)
  (do-another-thing)
  (return-final-value))

With side effects the earlier calls matter, so do exists: it evaluates several expressions and returns the last.

Where you actually need it is the useful part. if takes single expressions for its branches, so a branch that must do several things needs an explicit do:

(if (is-something-true?)
  (do
    (log-message "in true branch")
    (store-something-in-db)
    (return-useful-value)))

Function bodies and when already have an implicit do, which is why you rarely write it there.

Reader macros

Notation the reader expands before evaluation ever happens — the quote ', the backslash for characters, the semicolon for comments. They are the mechanism behind several things you have already used, and they return when we write macros.

Learning outcomes

  • structure-a-program: Define functions, bind locals with let, and sequence side effects with do.

Concepts

  • let-form: explains lexical local bindings and the underscore convention
  • symbols-and-keywords: uses symbols to create lexical names and var bindings for functions

Conditionals and logic

Five forms, two axes

(if test consequent alternative)
(if-not test consequent alternative)
(cond & clauses)
(when test & body)
(when-not test & body)

Rather than memorising five things, notice what separates them:

  • Is the test negated? if versus if-not, when versus when-not.
  • Is there an else branch? if has one and takes single expressions. when has none but takes a body, implicitly wrapped in do.

So: reach for when when there is no alternative and the body has several expressions, and you avoid writing do yourself.

(if (> 5 2) "yes" "no")
;=> "yes"
(if-not (> 5 2) "yes" "no")
;=> "no"

(cond
  (> x 0) "greater!"
  (= x 0) "zero!"
  (< x 0) "lesser!")
;=> "greater!"

(when (> 5 2)
  (println "five")
  (println "is")
  (println "greater")
  "done")
;=> "done"

Logical functions return values

This is the part that surprises people. and and or do not return true and false — they return the value that decided the result.

(and)
;=> true
(and :a :b :c)
;=> :c
(and :a nil :c)
;=> nil
(and :a false :c)
;=> false
(and 0 "")
;=> ""

and returns the last value if all are truthy, and the actual falsey value that stopped it otherwise — nil or false, whichever it met.

(or)
;=> nil
(or :a :b :c)
;=> :a
(or :a nil :c)
;=> :a
(or nil false)
;=> false
(or false nil)
;=> nil

or returns the first truthy value, or the last falsey one.

(not true)
;=> false
(not 1)
;=> false
(not nil)
;=> true

Comparisons chain, which is worth knowing:

(< 2 4 6 8)
;=> true

Returning values rather than booleans is what makes or usable for supplying defaults. Notice it now rather than being surprised later.

Contrasting = with == for numeric equality.

Learning outcomes

  • choose-a-conditional: Choose the right conditional form from if, if-not, cond, when and when-not.

Concepts

  • higher-order-sequence-functions: sets up the sequence-oriented functions the next section builds on

Functional iteration

Unit 1 said repetition happens by recursion rather than iteration. Here is the actual vocabulary — and there is more of it than you might expect.

while

(while test & body)
(while (request-on-queue?)
  (handle-request (pop-request-queue)))

The least Clojure-like of the group, useful for polling something that changes outside your control.

loop / recur

The primary stack-safe mechanism. loop establishes bindings like let; recur rebinds them and jumps back.

factorial implemented with loop/recur.
Importantrecur must be in tail position

The book’s fact-loop-invalid shows what happens otherwise:

(defn fact-loop-invalid [n]
  (loop [current n fact 1]
    (if (= current 1)
        fact
        (recur (dec current) (* fact current)))
    (println "Done, current value:" current)))

The recur is not the last thing the loop body does, so this is a compile-time error rather than a silent bug. That is the good outcome — the compiler catches it.

doseq and dotimes

For side effects rather than values:

(defn dispatch-reporting-jobs [all-users]
  (doseq [user all-users]
    (run-report user)))

(dotimes [x 5]
  (println "X is" x))

dotimes prints 0 through 4 and returns nil. Both return nil, which is the signal you called them for their effects.

The sequence functions

These are what you will actually reach for.

map applies a function across one or more sequences:

(map inc [0 1 2 3])
;=> (1 2 3 4)
(map + [0 1 2 3] [0 1 2 3])
;=> (0 2 4 6)
(map + [0 1 2 3] [0 1 2])
;=> (0 2 4)

With several sequences, each supplies an additional argument — and the result is as long as the shortest.

filter and remove are complementary, and comparing them makes the point:

(defn non-zero-expenses [expenses]
  (let [non-zero? (fn [e] (not (zero? e)))]
    (filter non-zero? expenses)))
;=> (-2 -1 1 2 3)

(defn non-zero-expenses [expenses]
  (remove zero? expenses))
;=> (-2 -1 1 2 3)

Same result, and remove needs no helper function at all.

reduce collapses a sequence to a value:

(defn factorial [n]
  (let [numbers (range 1 (+ n 1))]
    (reduce * numbers)))

for, the list comprehension

Not a loop, despite the name — it builds a sequence.

(def chessboard-labels
  (for [alpha "abcdefgh"
        num (range 1 9)]
    (str alpha num)))
;=> ("a1" "a2" "a3" ... "h7" "h8")

Multiple binding sequences nest. :when filters:

(defn primes-less-than [n]
  (for [x (range 2 (inc n))
        :when (prime? x)]
    x))

(primes-less-than 50)
;=> (2 3 5 7 11 13 17 19 23 29 31 37 41 43 47)

and both features combine:

(defn pairs-for-primes [n]
  (let [z (range 2 (inc n))]
    (for [x z y z :when (prime? (+ x y))]
      (list x y))))

Every one of these works on any collection, because of the sequence abstraction two sections back.

Learning outcomes

  • iterate-functionally: Replace imperative loops with recur, the sequence functions, and list comprehension.
  • use-the-sequence-abstraction: Use the ISeq abstraction that unifies every Clojure collection.

Concepts

  • loop-recur: explains stack-safe recursion and the tail-position requirement
  • higher-order-sequence-functions: details map, filter, remove, reduce and list comprehension

Threading macros

The last form of the unit, and the one that most changes how finished code looks.

The problem

Compound interest, written directly:

(defn final-amount [principle rate time-periods]
  (* (Math/pow (+ 1 (/ rate 100)) time-periods) principle))

(final-amount 100 20 1)
;=> 120.0
(final-amount 100 20 2)
;=> 144.0

Correct, and it reads inside-out: to follow it you find the innermost form and work outward, in the reverse of the order things actually happen.

Thread-first

-> takes the result of each form and inserts it as the first argument of the next:

(defn final-amount-> [principle rate time-periods]
  (-> rate
      (/ 100)
      (+ 1)
      (Math/pow time-periods)
      (* principle)))

(final-amount-> 100 20 1)
;=> 120.0
(final-amount-> 100 20 2)
;=> 144.0

Same results, read top to bottom in the order the steps occur.

as-> threading a map through a series of operations, for cases where the value does not belong in the first position.
NoteNotice what these are

The threading macros add no capability — they change legibility, and that is enough to make them idiomatic.

Notice also what they are: macros that rewrite one expression shape into another before evaluation. You have now used several — defn, when, cond, -> — without writing any. The macro units later in this module are about writing your own.

Learning outcomes

  • use-threading-macros: Flatten nested calls into a readable pipeline with the threading macros.

Concepts

  • threading-macros: introduces macros that pipeline nested operations into a readable linear order

What you can now write

That is the working vocabulary. It is a lot at once, and it is meant to be a reference you return to rather than something memorised in a sitting.

The through-line worth carrying forward: the sequence abstraction. Three functions defined in one short section are why map, filter, remove, reduce and for all work on anything you hand them.

Learning outcomes

  • work-at-the-repl: Evaluate expressions at the REPL and use its magic variables and documentation tools.
  • explain-prefix-notation-consequences: Explain why prefix notation makes code easy to generate and manipulate.
  • use-clojure-scalars: Work with Clojure’s scalar types: nil, booleans, characters, strings and the number tower.
  • distinguish-symbols-and-keywords: Distinguish a symbol from a keyword and say when to reach for each.
  • choose-a-collection: Choose between a list, a vector and a map, and use each one’s core operations.
  • use-the-sequence-abstraction: Use the ISeq abstraction that unifies every Clojure collection.
  • structure-a-program: Define functions, bind locals with let, and sequence side effects with do.
  • choose-a-conditional: Choose the right conditional form from if, if-not, cond, when and when-not.
  • iterate-functionally: Replace imperative loops with recur, the sequence functions, and list comprehension.
  • use-threading-macros: Flatten nested calls into a readable pipeline with the threading macros.

Concepts

  • repl: summarises the foundational role of REPL-based interactive programming
  • persistent-collections: recaps the core data types and abstractions before moving deeper

Conclusion

  • The REPL is where Clojure development happens, not an accessory to it.

    Its evaluate-then-print phases explain why println produces two lines. The magic variables and doc/find-doc/apropos are what let you answer your own questions without leaving it.

  • Prefix notation is regular, and regularity makes code easy to generate.

    add(1, 2) was already prefix. The payoff is that cond is a nested list of pairs — trivial to generate compared with a Java case. The macro units later depend entirely on this.

  • Clojure’s scalars are richer than they look.

    Everything but false and nil is true. Strings and characters are Java’s. Numbers form a tower with ratios and arbitrary precision, and contagiousness decides the type of a mixed result.

  • Symbols resolve; keywords evaluate to themselves.

    A symbol is an identifier that becomes the value it signifies. A keyword is the type built for being data, which is why map keys are keywords and not quoted symbols.

  • One sequence abstraction unifies every collection.

    first, rest and cons work on lists, vectors and maps alike. That is why the higher-order functions never need to know what they were handed.

  • Loops are replaced by a vocabulary, not a single construct.

    recur for stack-safe recursion, doseq/dotimes for effects, map/filter/remove/reduce for transformation, for for comprehension. Then -> to make the result readable.

Where next

The next unit, Building Blocks of Clojure, goes underneath what you have just learned: functions in depth including higher-order ones, let revisited with lexical closures, vars and binding, namespaces for organising code, destructuring for pulling structures apart inside a binding form, and the metadata and type-hint machinery that connects back to the JVM.