Clojure Elements: Data Structures and Functions

Clojure

2026-08-20 09:15

Where we are

Reading is not writing

Introducing Clojure taught you to read.

This unit gets you writing.

The longest, most reference-like unit in the module.

Type, do not read

Almost everything here is a form to try at the REPL.

The REPL is not an accessory to learning Clojure.

It is where the learning happens.

The four parts

  • Coding at the REPL
  • Data structures
  • Program structure
  • Program flow

By the end: the vocabulary to write small to medium programs.

Coding at the REPL

Hello, world — twice

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

Two lines. Why?

Evaluate, then print

println is side-effecting — it prints, then returns nil.

Hello, world! printed during the evaluation phase.

nil printed during the print phase.

The magic variables

  • *1, *2, *3 — last, second-last, third-last successful form
  • *e — the last error

Each success shifts the chain along.

Watch them shift

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

A subtlety

(def a-str *1)

This captures the value that was in *1.

*1 moves on. a-str does not.

On error

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

The numbered variables stay put. Only *e changes.

Answer your own questions

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

doc output for +.

Syntax revisited

You already write prefix notation

add(1, 2)

Function name first. Arguments after.

That is prefix notation.

Clojure just moves the parenthesis

(add 1 2)

And drops the comma — whitespace is enough.

Only the maths feels strange, because most languages special-case operators.

What regularity buys

it makes it easy to generate and manipulate code

cond is just a list

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

A nested list. An even number of expressions, in pairs.

Test, then result. Test, then result.

Compare

Generating that list: easy.

Generating a case statement in Java: not.

This is why the macro units later in this module are possible.

Commas are whitespace

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

The printer uses them when echoing maps — purely for your eyes.

Comments

;; This function does addition.
(defn add [x y]
  (+ x y))

One semicolon after code · two for a line · three for a block

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

Scalars and the number tower

Truthiness

Everything other than false and nil is considered true.

That is the whole rule.

Strings are Java strings

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

Both true. Note the leading periods — interop, from unit 1.

The number tower

Type Examples Contagiousness
Integer 42, 0x2a, 2r101010 0
Big integer 42N 1
Ratio 1/3, -2/4 2
Big decimal 2.78M 3
Floating point 2.78 4

(/ 4 9) gives the ratio 4/9.

Contagiousness in action

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

Overflow

user> (inc 9223372036854775807)
ArithmeticException integer overflow
user> (inc' 9223372036854775807)
;=> 9223372036854775808N

+' -' *' inc' dec' — a trailing quote means autopromote.

Symbols and keywords

Symbols are identifiers

In (+ 1 2), the + is a symbol signifying the addition function.

Reading and evaluating are separate, so a symbol has two aspects:

its existence as data, and the value it resolves to.

Valid and invalid

validfoo 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

'arglebarg
;=> arglebarg

The quote says: this is literal data, not code.

Keywords are built for that

A keyword is sort of like an autoquoted symbol.

Never references another value. Always evaluates to itself.

:foo :foo/bar :->foo :+

Converting

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

No namespace part → nil. name leaves strings unchanged.

Lists, vectors and maps

Lists: front only

Singly linked. Add and remove at the front.

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

The payoff: multiple lists can share tails.

Lists as stacks

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

Lists are special

(def three-numbers (1 2 3))

Fails. The compiler reads lists as code — it tries to call 1.

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

Vectors: indexed

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

They agree — until you go out of range:

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

Maps: two ways to look up

(def the-map {:a 1 :b 2 :c 3})

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

Both the map and the keyword are callable.

Maps: add and remove

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

New maps each time. Nothing was mutated.

Nested data

(assoc-in users [:kyle :summary :average :monthly] 3000)
(get-in users [:kyle :summary :average :monthly])
;=> 1000
(update-in users [:kyle :summary :average :monthly] + 500)

assoc-in creates missing intermediate maps.

update-in takes a function, not a value.

The sequence abstraction

Three functions

first · rest · cons

Every collection supports them.

Across collections

(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 to notice

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

A map seen as a sequence is a sequence of pairs.

(rest [])             ;=> ()

Not nil. So you can keep calling rest without a check.

Why this is the key section

map, filter, remove, reduce never need to know

whether they were handed a list, a vector, or a map.

One abstraction carries the whole module.

Structuring a program

defn is def plus fn

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

A function is a value bound to a name. Exactly as unit 1 promised.

let: before

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

let: after

(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. That is the point.

Bindings are sequential

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

And bind to _ when you need the binding but not the value.

Where do is needed

if takes single expressions for its branches:

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

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)

Negated? · Else branch and implicit do?

Choosing

if — has an alternative, takes single expressions

when — no alternative, but takes a body

So when saves you writing do.

and returns values

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

The last value if all truthy — otherwise the actual falsey value that stopped it.

or too

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

First truthy value, or the last falsey one.

This is what makes or usable for defaults.

Comparisons chain

(< 2 4 6 8)
;=> true

= versus == for numeric equality.

Functional iteration

The vocabulary

  • while — polling something outside your control
  • loop/recur — stack-safe recursion
  • doseq, dotimes — side effects
  • map, filter, remove, reduce — transformation
  • for — list comprehension

loop / recur

factorial with loop/recur.

recur must be in tail position

(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 last. Compile-time error.

Which is the good outcome — the compiler catches it.

Side effects

(doseq [user all-users]
  (run-report user))

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

Both return nil. That is the signal you called them for their effects.

map

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

Multiple sequences → multiple arguments. Result is as long as the shortest.

filter, then remove

(defn non-zero-expenses [expenses]
  (let [non-zero? (fn [e] (not (zero? e)))]
    (filter non-zero? expenses)))
(defn non-zero-expenses [expenses]
  (remove zero? expenses))

Same result. No helper function at all.

reduce

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

for is not a loop

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

It builds a sequence. Multiple bindings nest.

for with :when

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

Threading macros

Inside-out

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

Correct — and read in reverse of the order things happen.

Top to bottom

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

Each result threads into the first argument of the next form.

Same answers

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

as->, for when the value does not belong in first position.

Notice what these are

They add no capability. They change legibility.

And they are macros — rewriting one expression shape into another before evaluation.

defn · when · cond · -> — you have used several already.

Summary

The through-line

The sequence abstraction.

Three functions in one short section are why map, filter, remove, reduce and for work on anything you hand them.

The six things to carry away

  • The REPL is where the work happens; the magic variables and doc make it explorable.
  • Prefix notation is regular, and regularity makes code generatable.
  • Everything but false and nil is true; numbers form a tower with contagiousness.
  • Symbols resolve; keywords evaluate to themselves.
  • first, rest, cons unify every collection.
  • Loops become a vocabulary — recur, the sequence functions, for, then ->.

Where next

Building Blocks of Clojure goes underneath all of this.

Higher-order functions · lexical closures · vars and binding

namespaces · destructuring · metadata and type hints