Lecture notes — Introducing Clojure

Published

2026-08-20 00:00

Keywords

ver. 1.0.0

← Introducing Clojure

Where we are

This is the first unit of the module, so nothing is assumed from an earlier one. What we do assume is that you have written programs in a mainstream imperative language — Java, Python, C#, Ruby — and are comfortable with variables, objects, methods and loops.

Bring that experience but hold it loosely. The book is blunt that most of the difficulty ahead is unlearning, not learning.

What you will be able to do

  1. place-clojure-in-the-lisp-family — Place Clojure in the Lisp family and say what it inherits and what it changes.
  2. define-functional-programming-requirements — State the minimum requirement for a functional language and the three features that usually accompany it.
  3. explain-structural-sharing — Explain how persistent data structures stay fast without copying.
  4. separate-value-from-identity — Distinguish a value, which never changes, from an identity, which may hold different values over time.
  5. read-prefix-notation — Read and write Clojure’s prefix notation, including nested and variable-arity calls.
  6. explain-the-two-uses-of-parentheses — Explain the two purposes parentheses serve, and why the second one matters later.
  7. use-java-interop-syntax — Call Java static members, instance methods and constructors from Clojure.
  8. explain-why-clojure-is-hosted — Explain what hosting on the JVM buys and what it obliges you to learn.
  9. explain-jvm-threads-and-clojure-primitives — Explain why JVM threads matter for Clojure’s concurrency story.

What we will cover

  • Lisp syntax and prefix notation — parentheses, no operators, variable arity.
  • First-class functions — functions as values, purity, referential transparency.
  • Persistent data structures — immutability made fast by structural sharing.
  • Identity, state and concurrency — separating values that cannot change from identities that can.
  • Hosted language interop — Clojure on the JVM, and calling Java directly.

Three pillars at once

Clojure is a simple and succinct programming language designed to leverage easily both legacy code and modern multicore processors.

Each half of that sentence has a source:

  • simplicity comes from a sparse and regular syntax
  • succinctness comes from dynamic typing and functions-as-values
  • it uses existing Java libraries because it is hosted on the JVM
  • it simplifies multithreaded programming through immutable data structures and concurrency constructs

Clojure’s strengths do not lie on a single axis, which is exactly what makes it awkward to introduce. As a hosted language it takes the technical strengths of platforms like the JVM and adds the “succinctness, flexibility, and productivity” of a dynamically typed language. As a functional language, its high-performance immutable data structures produce simpler programs that are easier to test and reason about — and that pervasive immutability is also central to its concurrency constructs. As a Lisp, its syntax brings an elegant simplicity and powerful metaprogramming tools.

So there are three pillars, and they cannot really be separated: Lisp, functional programming, and the JVM. At every step they play on each other. We take them one at a time anyway, because we have to start somewhere.

ImportantWhat is actually hard about this

Not the syntax, and not the JVM. The book is explicit:

The truly mind-bending part of coding in Clojure comes from the shift from an imperative mindset to a functional programming approach to program design.

Much of your early time will go on wondering how to do things in Clojure you can already do easily elsewhere. Most people spend more time unlearning idioms from other languages than learning Clojure’s own.

Learning outcomes

  • place-clojure-in-the-lisp-family: Place Clojure in the Lisp family and say what it inherits and what it changes.
  • define-functional-programming-requirements: State the minimum requirement for a functional language and the three features that usually accompany it.
  • explain-why-clojure-is-hosted: Explain what hosting on the JVM buys and what it obliges you to learn.

Clojure as a modern Lisp

Clojure is a fresh take on Lisp, one of the oldest programming language families still in active use — second only to Fortran.

Lisp is not a single language. It is a style of programming language, designed in 1958 by Turing award winner John McCarthy. The family today consists primarily of Common Lisp, Scheme and Emacs Lisp, with Clojure as one of the newest additions.

It is not a museum piece. Despite a fragmented history, Lisp implementations run cutting-edge systems: NASA’s Pathfinder mission-planning software, algorithmic trading at hedge funds, flight-delay prediction, data mining, natural language processing, expert systems, bio-informatics, robotics, electronic design automation, web development, next-generation databases.

What Lisp gave everyone else. Lisp has the reputation of being a dark art and a secret weapon, and it was the birthplace of language features you now take for granted:

  • conditionals
  • automatic garbage collection
  • macros
  • functions as language values — not merely procedures or subroutines

Where Clojure sits. It belongs to the family but adheres to no existing implementation exclusively, preferring to combine the strengths of several Lisps together with features from ML and Haskell. What it builds on top of the tradition: a pragmatic approach to functional programming, a symbiotic relationship with existing runtimes like the JVM, and built-in concurrency and parallelism support.

Learning outcomes

  • place-clojure-in-the-lisp-family: Place Clojure in the Lisp family and say what it inherits and what it changes.

Concepts

  • lisp-syntax-prefix-notation: traces Clojure’s lineage back to McCarthy’s Lisp family and the features that family contributed

What makes a language functional

Functional programming languages have seen an explosion in popularity — Haskell, OCaml, Scala and F# rising from obscurity, while C/C++, Java, C#, Python and Ruby borrowed their features. With that much activity, it can be difficult to determine what actually defines a functional language. So start with the minimum.

The minimum requirement

The minimum requirement to be a functional language is to treat functions as something more than named subroutines for executing blocks of code.

Functions in an FP language are values, just as the string "hello" and the number 42 are values. You can pass functions as arguments to other functions, and functions can return functions as output. A language that can treat a function as a value is said to have first-class functions.

The three features that come with it

Most FP languages also include:

  • Pure functions with referential transparency
  • Immutable data structures as the default
  • Controlled, explicit changes to state

These three are interrelated, and it is worth seeing how.

A function is pure if it has no side effects on the world around it — no changing global state, no I/O. It is referentially transparent if the same inputs always produce the same output. Functions that behave this way are simple, and it is easier to reason about code that behaves consistently without respect to the implicit environment it runs in.

Making immutable data structures the language default is what makes this practical rather than merely possible: it guarantees that functions cannot alter the arguments passed to them. In a simplistic sense, it is as if arguments are always passed by value and never by reference.

NoteClojure is pragmatic, not pure

Functional languages are often judged by their “purity”. Clojure’s default patterns encourage pure FP — immutable structures, higher-order functions and recursion in place of imperative loops, even a choice between lazy and eager evaluation of collections.

But certain tasks are more clearly modeled with mutable state and an imperative approach, so Clojure provides well-defined constructs for sharing and changing state. It also does not require you to annotate code that causes side effects, as some purer languages do.

The same pragmatism explains the host: when necessary you can drop down to Java APIs directly, with all the performance and all the pitfalls that brings.

Learning outcomes

  • define-functional-programming-requirements: State the minimum requirement for a functional language and the three features that usually accompany it.

Concepts

  • first-class-functions: defines first-class functions and referential transparency as the core requirements of a functional language

Persistent structures and structural sharing

The previous section provokes an immediate objection, and the book states it in the reader’s own voice:

“Hold on,” you might say, “passing arguments by value and copying data structures everywhere is expensive, and I need to change the values of my variables!”

The answer

Clojure’s immutable data structures are based on research into performant, purely functional data structures designed to avoid expensive copying.

In theory, changing an immutable structure gives a brand-new structure, because you cannot change what is immutable. In reality, Clojure employs structural sharing and other techniques so that only the minimum amount of copying is performed, and operations stay fast and conserve memory.

In effect, you get the safety of passing by value with the speed of passing by reference.

Trace it on a tree

Persistent data structures cannot be changed, but you can see how one might be “edited”.

The tree xs, of immutable nodes (circled letters) and references (arrows).

The new tree ys, sharing everything it can with xs.

The tree xs consists of immutable nodes and references, so it is impossible to add or remove a value from it. But you can create a new tree that shares as much of xs as possible.

Adding a new value e creates a new set of nodes and references on the path to the rootd', g', f' — that reuse the old nodes b, a, c and h, resulting in the new persistent tree ys.

Only the path to the root is new. Everything off that path is shared. That is the basic principle underlying all of Clojure’s persistent data structures.

The technique comes from Chris Okasaki’s Purely Functional Data Structures (1996), which is the standard reference if you want the algorithms rather than the intuition.

Learning outcomes

  • explain-structural-sharing: Explain how persistent data structures stay fast without copying.

Concepts

  • persistent-data-structures: explains persistent trees and the structural sharing that makes immutability affordable

Values, identities, and controlled change

Things in your programs change. Most languages have variables that serve as named pieces of state you can change at any time. In Clojure the story is more controlled and better defined, and it rests on a distinction worth getting exactly right.

A value cannot change. The number 42 cannot change; 42 is 42, and subtracting 2 from it does not change 42 but gives a new value, 40. This truth extends to all values, not just numbers.

An identity can. If you have a variable acting as the identity for something in your program, holding 42 initially, you might want to assign a new value later. In that case the variable is a container into which you may put different values at different times.

What Clojure guarantees about the change

In a multithreaded, concurrent world, your language should give you assurances about how those changes take place:

  • One variable, atomically.

    Multiple threads always get a consistent picture, and when it changes it does so in a single, indivisible operation. Threads reading during an atomic change get the last value from before it began; threads trying to write are held off until it completes.

  • Several variables together, transactionally.

    Clojure’s software transactional memory (STM) changes multiple variables as part of a transaction and rolls back if they do not all complete as expected.

  • On another thread, without blocking.

    If a change should happen off the main thread, Clojure provides facilities for that too.

All of these are built into the core of the language. The book’s summary is worth quoting: concurrency is so easy you have to work to make your programs not support it.

NoteNamed here, taught later

We are only naming these. The State and the Concurrent World unit later in this module spends its entire length on refs, agents, atoms and vars, with code.

Learning outcomes

  • separate-value-from-identity: Distinguish a value, which never changes, from an identity, which may hold different values over time.

Concepts

  • identity-state-concurrency: introduces the separation of immutable values from mutable identities, and the constructs that manage change

Prefix notation

Clojure’s syntax is derived from its Lisp roots: lots of parentheses. It is alien to most developers with experience in Algol-inspired languages. The book offers four steps for getting over the hump:

  • initially ignore the parentheses
  • consider how other languages use parentheses
  • see parentheses as units of value, or expressions
  • embrace the parentheses

Ignore them for a moment

(get-url "http://example.com")

If you guessed this makes an HTTP request, you are correct. get-url is not a built-in — it is a self-describing name we will reuse later.

Now some real ones:

(str "Hello, " "World!")
;; Result: "Hello, World!"
; (A Semi-colon starts a code comment which continues
; to the end of the line.)

str concatenates its arguments into a single string. Other languages generally use an operator:

"Hello from " + "a language " + "with operators";
;; Result: "Hello from a language with operators"

That is infix notation — the operator goes in between. Clojure uses prefix notation for all functions, and for everything that looks like an operator, so concatenating more strings just means passing more arguments:

(str "Hello from " "Clojure with " "lots of " " arguments")
;; Result: "Hello from Clojure with lots of arguments"

(+ 1 2)
;; Result: 3
(+ 1 2 3)
;; Result: 6

Two advantages

  • There are no operators. str and + are both regular Clojure functions; one just happens to have a nonalphabetic character as its name. There is no system of operator precedence to memorise.

    Clojure doesn’t have operators. That is the whole rule.

  • Variable arity is natural. Because you do not interleave operators between arguments, it is natural for such functions to take an arbitrary number of them.

    You can add another argument without fear of forgetting to put an operator between each one.

Now nest them

In a language with operators you might write 3 + 4 * 2 and need to remember precedence. You can disambiguate with parentheses — 3 + (4 * 2) — and Clojure makes that level of explicitness a requirement:

(+ 3 (* 4 2))
;; Result: 11

Break it down. The outermost function is +, with two arguments: 3 and the form (* 4 2). Solve the inner form first — calling * with 4 and 2 gives 8 — and rewrite:

(+ 3 (* 4 2))
(+ 3 8)
;; Result: 11

The trade-off is real and worth naming: operators and precedence make mathematical expressions more concise in other languages, and Clojure makes calling functions completely consistent across the language.

Learning outcomes

  • read-prefix-notation: Read and write Clojure’s prefix notation, including nested and variable-arity calls.

Concepts

  • lisp-syntax-prefix-notation: introduces prefix notation, variable arity, and the elimination of operator precedence

What parentheses are for

We stopped ignoring them. For the sake of reading and writing your first programs, parentheses serve two purposes: calling functions and constructing lists.

Calling functions

Everything so far has been the first purpose. Inside a set of parentheses, the first language form is always a function, macro, or special form, and all subsequent forms are its arguments. Macros and special forms can be thought of, for now, as functions that get special treatment.

Parentheses for calling functions.

Nested parentheses for calling functions.

The book offers a mnemonic worth adopting:

Start training your brain to associate left parenthesis with function invocation. That left parenthesis is like a phone being held up to the function’s ear, getting ready to call it with the rest of the items up to the matching right parenthesis.

It becomes increasingly important once we get to higher-order functional patterns. And remember that arguments will not always be simple values — often they are nested expressions, as the second figure shows.

Constructing lists

At once the most common and the least noticeable use. Clojure has literal syntax for collections other than lists, and idiomatic programs use all the collection types based on their performance strengths — Clojure is not as list-centric as other Lisps.

But at the meta level:

your entire Clojure program is a series of lists: the very source code of your program is interpreted by the Clojure compiler as lists that contain function names and arguments that need to be parsed, evaluated, and compiled.

Because the same language features are available at the compiler level and in normal program code, Lisp enables uniquely powerful metaprogramming.

That single fact is what the two macro units later in this module are built on. It is worth marking now, because nothing else in this unit depends on it.

TipDo not match parentheses by hand

Because parentheses contain all expressions, you edit Clojure by arranging expressions like building blocks — each a self-contained world of functionality that results in a consistent value and can go anywhere that value is required.

That consistency means editors can provide structural editing for moving expressions around, so you never have to check that your parentheses are matched. Learning those tools is what turns the parentheses from a hindrance into an advantage.

Learning outcomes

  • explain-the-two-uses-of-parentheses: Explain the two purposes parentheses serve, and why the second one matters later.
  • read-prefix-notation: Read and write Clojure’s prefix notation, including nested and variable-arity calls.

Concepts

  • lisp-syntax-prefix-notation: explains the dual role of parentheses as invocation and as list construction

The JVM underneath

Clojure does not hide the host platform on which it is implemented. Because it embraces its host instead of hiding it, you must learn the basics of Java and the JVM to code in Clojure.

Java is three things

Java is three distinct pieces that were designed and shipped together:

  • a language
  • a virtual machine
  • a standard library

Parts of Clojure are written in the Java language, but Clojure itself does not use it — Clojure code compiles directly to JVM bytecode. Clojure does require the standard library for many basic functions, and because that library was written in and for Java, some knowledge of the language helps you use it.

Where the seam shows

  • Clojure uses Java types directly. Strings are Java String objects, literal numerals are Java Long objects, and Clojure’s collections implement the same interfaces Java collections do. That reuse cuts both ways: Java code can consume Clojure’s immutable data structures seamlessly.
  • Sometimes Clojure wraps Java. Many functions in clojure.string delegate to methods in Java’s String class.
  • Often there is no wrapper. Clojure does not implement functions for abs, exp, log, sin, cos and tan — they live in java.lang.Math and must be invoked through interop.

Java’s object model, briefly

Java is object-oriented, based on a class hierarchy with single inheritance. Common behaviours can be grouped into interfaces, which are outlines of method signatures that implementing classes must support.

Only one public class or interface per file, and files must be on the classpath — a collection of directories the compiler searches, akin to C’s search path or Ruby’s $LOAD_PATH. A fully qualified name is the package name followed by the class name, so java.lang.Math and com.mycompany.Math can coexist.

What this has to do with Clojure: everything in java.lang is imported by default in all Clojure programs, which is why you can write String and Integer rather than java.lang.String. Many Clojure collections implement Java interfaces — all of them implement java.lang.Iterable or java.util.Collection, and some implement java.util.List or java.util.Map depending on purpose. The Clojure compiler also expects your source on the classpath and namespace names to be unique.

Why hosted at all

Rather than pairing a language design with a new runtime, Rich Hickey focused on Clojure-the-language and relied on existing VMs. He began on the JVM; Clojure has since reached the CLR (Clojure-CLR) and JavaScript engines (ClojureScript).

The book calls this the best kind of engineering laziness. The JVM is mature and ubiquitous with a myriad of third-party libraries; HotSpot is open source, with an advanced JIT compiler and a choice of garbage collectors, competitive with native runtimes.

By taking these features for granted as part of the underlying runtime host, the Clojure community is free to focus its time on a solid language design and higher-level abstractions instead of reinventing the VM wheel (and the bugs that come with it).

There is a business case too: relying on an existing VM lowers the risk of introducing Clojure into an organisation with existing architecture and expertise.

What you get for the obligation: Joda Time for dates, JDBC drivers, Jetty as an embeddable web server, Bouncy Castle for cryptography, Selenium WebDriver, the Apache Commons libraries — plus JVM monitoring tools and profilers like VisualVM, YourKit and New Relic.

Learning outcomes

  • explain-why-clojure-is-hosted: Explain what hosting on the JVM buys and what it obliges you to learn.

Concepts

  • hosted-language-interop: details the rationale for hosting, Java’s object model, and how Clojure imports it

Calling Java from Clojure

The dot operator — written as a literal . — forms the basis for Java interop. Seen by itself after an opening parenthesis, read it as “in the scope of A do B with arguments…”.

(. Math PI)
;; Result: 3.141592653589793
(. Math abs -3)
;; Result: 3
(. "foo" toUpperCase)
;; Result: "FOO"

The sugared forms

Outside interop, the first form in an expression is a function, macro or special form — so Clojure provides syntactic sugar to make interop look more idiomatic.

Static members use a forward slash:

Math/PI
;; Result: 3.141592653589793
(Math/abs -3)
;; Result: 3

Static fields and methods are defined on the class rather than on instances. PI is a field, so it needs no parentheses to return a value; abs is a method, so it must still be invoked.

Instance methods use a leading dot:

(.toUpperCase "foo")
;; Result: "FOO"

Constructors use new or a trailing dot:

(new Integer "42")
;; Result: 42
(Integer. "42")
;; Result: 42
NoteWhy the sugar is safe

The trailing dot, the leading dot, and the forward slashes are all syntactic conveniences. During Clojure’s macro expansion phase, the trailing dot expands to the new special form and the others expand to the standalone dot form — so they are all literally equivalent by the time your code is evaluated.

This is the first place in the module where macro expansion is doing visible work for you. It will not be the last.

Note the scope of what we have covered. The dot operator provides a doorway for consuming Java APIs. Extending Java’s class system is a later topic, as are the design abstractions Clojure provides in spite of the object-oriented nature of its host.

Learning outcomes

  • use-java-interop-syntax: Call Java static members, instance methods and constructors from Clojure.

Concepts

  • hosted-language-interop: gives the syntax for calling Java methods, fields and constructors

Threads and the concurrency story

A thread represents program execution. Every program, in any language, has at least one main thread in which application code is evaluated, and runtimes generally provide a way to start more.

Why the JVM matters here. The default runtimes for Ruby and Python provide lightweight or “green” threads managed entirely by the runtime. JVM threads map directly to native system threads, which means they take advantage of multiple CPU cores for free by letting the operating system schedule them. By engaging all available cores, the JVM provides genuine and performant parallelism.

The problem that creates

With a single thread, the program is evaluated serially and it is relatively simple to understand when objects are created, changed and destroyed. Introduce threads running at the same time and concurrency issues arrive:

  • if state can be accessed from multiple threads simultaneously, how can you be sure two are not changing it at once?
  • can changes be performed atomically, so no thread sees a corrupt “in progress” state?

Java has all the tools necessary to write safe concurrent programs with shared mutable state. And:

in practice it’s extremely difficult to write such programs correctly.

Having spent years writing them himself, Rich Hickey implemented a set of constructs in Clojure that not only allow correctness but enforce it at the language level.

What Clojure provides

Because Clojure’s core data structures are all immutable, the issue of shared mutable state becomes largely moot to begin with. Where mutable state is required:

  • vars, atoms, refs and agents — concurrency data structures with clearly defined semantics for how the underlying state changes
  • fast access even mid-change — Clojure maintains a snapshot of old values while a change is in progress, so reading never blocks on a write
  • futures and promises — for use cases needing parallel execution but not shared state, implemented with JVM threads rather than bound to a callback as is common in JavaScript

We stop at naming them. Comparing concurrency constructs in the abstract, or listing every function, teaches very little — the later unit does it with code.

Learning outcomes

  • explain-jvm-threads-and-clojure-primitives: Explain why JVM threads matter for Clojure’s concurrency story.
  • separate-value-from-identity: Distinguish a value, which never changes, from an identity, which may hold different values over time.

Concepts

  • identity-state-concurrency: introduces vars, atoms, refs and agents as the constructs for managed change
  • hosted-language-interop: explains how Clojure maps JVM threads to native OS threads for real parallelism

What you can now read

We have completed the transit around the three basic pillars: functional programming with immutable data structures, Lisp syntax, and host interop.

You now know the absolute basics of reading Lisp and Java interop code, which means you can go on to explore Clojure’s functions and data structures and worry about the underlying platform only when the need arises.

Learning outcomes

  • place-clojure-in-the-lisp-family: Place Clojure in the Lisp family and say what it inherits and what it changes.
  • define-functional-programming-requirements: State the minimum requirement for a functional language and the three features that usually accompany it.
  • explain-structural-sharing: Explain how persistent data structures stay fast without copying.
  • separate-value-from-identity: Distinguish a value, which never changes, from an identity, which may hold different values over time.
  • read-prefix-notation: Read and write Clojure’s prefix notation, including nested and variable-arity calls.
  • explain-the-two-uses-of-parentheses: Explain the two purposes parentheses serve, and why the second one matters later.
  • use-java-interop-syntax: Call Java static members, instance methods and constructors from Clojure.
  • explain-why-clojure-is-hosted: Explain what hosting on the JVM buys and what it obliges you to learn.
  • explain-jvm-threads-and-clojure-primitives: Explain why JVM threads matter for Clojure’s concurrency story.

Conclusion

  • Clojure’s strengths come from three pillars that cannot be separated.

    Lisp syntax gives metaprogramming, functional programming gives immutability and simpler reasoning, and the JVM gives libraries and real threads. Each explains the others; none is complete alone.

  • A functional language treats functions as values, and everything else follows.

    First-class functions are the minimum bar. Purity, referential transparency and immutable defaults are the features that usually accompany it, and they are interrelated rather than independent.

  • Immutability is affordable because of structural sharing.

    Editing a persistent tree builds a new path to the root and reuses every untouched node. You get the safety of passing by value with the speed of passing by reference.

  • Values never change; identities hold different values over time.

    This is the distinction the whole concurrency story rests on. Once values cannot change, the only thing needing well-defined semantics is when an identity takes a new one.

  • Prefix notation removes operators, and parentheses do two jobs.

    No precedence to memorise, and variable arity for free. Parentheses call functions — and construct the lists your program is literally made of, which is what makes macros possible.

  • Clojure does not hide the JVM, and that is a deliberate trade.

    You must learn some Java. In exchange you get its libraries, its tooling, and native threads that give genuine parallelism.

Where next

The next unit, Clojure Elements: Data Structures and Functions, gets you to the REPL. You will meet Clojure’s core data structures, program structure and program flow — the point at which you can start writing small programs rather than only reading them.