Clojure — Reference
Source: https://clojure.org/reference
Clojure
- Created: 2007 by Rich Hickey
- Latest stable: 1.12.4 (December 10, 2025); ClojureScript and ClojureCLR track separately
- Paradigms: Functional-first, dynamic, Lisp dialect, immutable-by-default, hosted
- Typing: Dynamic, strong; optional gradual typing via
clojure.spec,Malli, orTyped Clojure - Memory: Host GC (JVM/V8/CLR); persistent (immutable) data structures with structural sharing (HAMT)
- Compilation: Compiled to host bytecode at load time (JVM bytecode for Clojure, JS for ClojureScript, IL for ClojureCLR); AOT compilation supported
- Primary domains: Backend services, data engineering, fintech, web (Ring/Reagent), build pipelines, datalog/query layers
- Official docs: https://clojure.org/reference and https://clojure.org/guides/getting_started
At a glance
Clojure is a modern Lisp hosted on the JVM (with first-class siblings for JavaScript and .NET). Code is data (homoiconic): every program is a Clojure data structure read by the reader and macro-expanded before compilation. The language emphasizes immutable persistent collections, pure functions, explicit state via reference types (atom/ref/agent), and seamless host interop. Stewarded by the core team at Nubank (which acquired Cognitect in 2020).
Getting started
Install — Use the Clojure CLI (clj/clojure) installer from https://clojure.org/guides/install_clojure. Requires Java 11+. Verify: clojure -M -e "(println :ok)".
Hello world (REPL):
(println "Hello, world!")As a script (hello.clj): clojure hello.clj.
Project (deps.edn) — clj -Tnew :template app :name my/app (uses tools.build/deps-new):
my-app/
deps.edn # deps + aliases
src/my/app.clj
test/my/app_test.clj
build.clj # tools.build
deps.edn is the modern way; Leiningen (project.clj) is the older convention but still widely used.
REPL — clj starts a REPL. (doc map), (source map), (dir clojure.string), (find-doc "regex"). Connect editor (Cursive, CIDER, Calva, Conjure) for inline eval — REPL-driven development is the cultural norm.
Basics
Types/literals — Long, BigInt (42N), Double, BigDecimal (1.5M), Ratio (22/7), String, Character (\a), Boolean, nil, Keyword (:foo, ::ns/foo), Symbol ('x), Regex (#"\d+"). Collections: list (1 2 3), vector [1 2 3], map {:a 1 :b 2}, set #{1 2 3}. All collection literals create persistent immutable values.
Variables/scoping — def creates a Var (namespace-global, mutable in dev — see alter-var-root). let for local bindings: (let [x 1, y 2] (+ x y)). binding for thread-local dynamic scope on ^:dynamic Vars.
Control flow — if, when, cond, case, condp, loop/recur (tail-recursion without consuming stack — JVM has no TCO). No statements; everything returns a value.
Functions — (defn add [a b] (+ a b)). Anonymous: (fn [x] (* x x)) or shorthand #(* % %). Multiple arities and varargs:
(defn greet
([] (greet "world"))
([name] (str "Hello, " name)))apply, partial, comp, juxt — first-class function combinators. Threading macros: -> (thread first), ->> (thread last), as->, some->.
Strings — Java String underneath. clojure.string namespace: (str/split s #","), (str/join "," xs), (str/replace s old new). Concatenation via (str a b c).
Collections — All seq-able. Core sequence functions (map, filter, reduce, take, drop, partition, group-by, frequencies, interleave) are lazy and uniform across vectors/maps/sets/lists/strings/Java arrays. Map access: (:key m) or (get m :key default).
Intermediate
Type system depth — Dynamic, but optional layers exist: clojure.spec.alpha (predicate-based contracts and generators) for runtime validation and property-based testing; Malli (community) for data-driven schemas with inference and generative testing; Typed Clojure / core.typed for gradual static checking.
Modules — Files map to namespaces: (ns my.app (:require [clojure.string :as str] [clojure.set :refer [union]])). :import for Java classes, :refer-clojure :exclude to shadow core.
Error handling — Java exceptions: (throw (ex-info "msg" {:data 1})), (try ... (catch Exception e ...) (finally ...)). ex-info attaches a data map and optional cause — idiomatic for typed-ish errors. Functional alternative: return [result error] tuples or Either-style maps.
Concurrency primitives — Four reference types with different semantics: atom (uncoordinated, sync, retry-on-conflict via CAS), ref (coordinated, sync, STM), agent (uncoordinated, async, queued), Var (thread-local via binding). core.async (https://github.com/clojure/core.async) adds CSP-style channels and go blocks. pmap, future, promise, delay for lightweight parallelism.
I/O — clojure.java.io (reader, writer, file, copy, resource), slurp and spit for one-liner read/write, with-open macro for resource cleanup. JDBC via next.jdbc.
Stdlib highlights — clojure.core (300+ functions), clojure.string, clojure.set, clojure.walk, clojure.zip, clojure.java.io, clojure.edn (data format reader, safe), clojure.pprint, clojure.test, clojure.spec.alpha, clojure.data (diff). Plus contrib: core.async, core.match, core.logic, data.json.
Advanced
Memory/GC — Inherits the host GC (HotSpot G1/ZGC on JVM, V8 on JS, CLR on .NET). Persistent collections use HAMT (Hash Array Mapped Trie) and RRB trees: structural sharing means assoc on a 1M-element map allocates only O(log32 n) ~6 nodes. Transients (transient/persistent!) provide a controlled mutable window for hot inner loops.
Concurrency deep dive — STM (dosync) coordinates multiple ref updates atomically; conflicts retry. commute allows order-independent updates without retry. core.async go-blocks compile (via macroexpansion) into state machines that park on channel ops without blocking OS threads. clojure.core.async/<!, >!, alts!, pipeline, mult, mix, pub/sub. JVM virtual threads (Project Loom) interop seamlessly via (Thread/startVirtualThread #(...)) on Java 21+.
FFI/host interop — Direct: (Math/sqrt 2) static call, (.length s) instance method, (String. "abc") constructor, (.-x point) field. proxy extends classes anonymously, reify implements interfaces, gen-class generates a named class (AOT). deftype and defrecord create JVM classes with optional protocol implementations.
Reflection — Costs at runtime; *warn-on-reflection* flag emits warnings. Type-hint with ^String s or ^"[B" (byte array). clojure.reflect/reflect, Java reflection still available.
Performance tools — criterium (microbench, the de facto choice), clj-async-profiler (FlameGraph from async-profiler), VisualVM, YourKit, JFR. time macro for quick measurement. *unchecked-math*, *warn-on-reflection*, primitive type hints, definterface, deftype with mutable fields for hot paths.
God mode
Macros — Macros run at read/compile time, transforming code-as-data. defmacro:
(defmacro unless [test & body]
`(if (not ~test) (do ~@body)))Backtick (`) is syntax-quote (resolves symbols to fully qualified names — hygienic-ish), ~ unquotes, ~@ splices. macroexpand-1, macroexpand, clojure.walk/macroexpand-all to inspect. gensym / # suffix (x#) for hygienic locals.
Multimethods — (defmulti area :shape) plus (defmethod area :circle [s] ...) does open dispatch on an arbitrary dispatch fn (not limited to first-arg type). Supports an isa?-based class hierarchy via derive/underive.
Protocols + records — defprotocol is single-dispatch polymorphism that compiles to JVM interface call (fast). defrecord creates a typed map with protocol implementations. extend-protocol, extend-type, satisfies?. Solves the expression problem.
Transducers internals — A transducer is a function (rf -> rf) from a reducing function to a reducing function. (map f), (filter pred), (take n) return transducers when called with one arg. comp composes them inside-out. transduce, into, sequence, eduction consume them. Decouples what (transformation) from where (collection/channel/sequence).
core.async go-block transformation — (go ...) macro performs a CPS (continuation-passing-style) transformation on its body, identifying <!/>!/alts! parking points and rewriting the block into a state machine implementing clojure.core.async.impl.protocols/Handler. Result: M:N scheduling of “lightweight processes” on a small thread pool.
ClojureScript compiler internals — cljs.compiler reads forms, expands macros (which run on the JVM), emits JavaScript via the Google Closure Compiler. Advanced compilation does whole-program dead-code elimination and renaming, often producing tiny bundles. Two build tools: shadow-cljs (de facto modern) and figwheel-main.
deps.edn vs Leiningen — deps.edn (CLI, modern) describes deps and aliases declaratively, no build automation built in (use tools.build or invoke clj -X for tasks). Leiningen (project.clj) is older, batteries-included (uberjars, repl, plugins, lein new); still common.
REPL-driven development — Workflow: keep an editor connected to a long-running REPL, eval forms inline as you type, redefine functions while the system runs, attach to production REPLs (carefully — see Nubank Datomic Cloud REPL practice). Tools: nREPL protocol, CIDER (Emacs), Calva (VS Code), Cursive (IntelliJ), Conjure (Neovim).
atom/ref/agent semantics — atom: spin-loop compareAndSet on an AtomicReference; swap! retries until success (must be pure). ref: STM-managed; reads are snapshot-isolated, writes commit atomically. agent: queued; send enqueues a fn for sequential application on a thread pool, errors hold the agent.
Persistent data structures (HAMT) — Hash maps and sets use a Bagwell HAMT (Hash Array Mapped Trie) with branching factor 32. Vectors use a 32-way trie + tail buffer. assoc paths copy O(log32 n) nodes; everything else shares. RRB trees back vector concat in O(log n).
Dynamic vars — ^:dynamic Vars allow (binding [*var* val] ...) to thread-locally rebind. Foundation of *out*, *err*, *ns*, request context in Ring middlewares.
Idioms & style
- Naming: kebab-case for everything (
my-fn-name),?for predicates (empty?),!for side-effects (swap!,reset!),*earmuffs*for dynamic vars,->prefix for type-converters (->Map). - Formatter:
cljfmt,zprint,cljstyle. Editor configs (cider/Calva) auto-format on save. - Linter:
clj-kondo(essential — fast, catches arity mismatches, unused deps, suspicious patterns). - Idiomatic: prefer maps and keywords over classes; thread data through pure transformations (
->,->>); push side effects to the edges; small functions; let collections be the API;ex-infofor errors; design data first then functions. - Expert review focus: laziness escaping a
with-openblock (returns a closed handle); mutating Java arrays without copying; reflective Java calls (no^Typehints); rebinding dynamic Vars across thread boundaries (usebound-fn); over-use of macros where a fn would do; AOT compile leaking into libraries.
Ecosystem
- Web: Ring (HTTP abstraction, the WSGI/Plug equivalent), Compojure / Reitit (routing), Pedestal, Aleph (async, Netty), Liberator, Luminus (template).
- Frontend: ClojureScript + Reagent (React wrapper), re-frame (Redux-like), Helix, Fulcro, Electric Clojure (Hyperfiddle, full-stack reactive).
- Data/DB: next.jdbc (SQL), HoneySQL (query DSL), HugSQL, Datomic (Hickey/Cognitect — datalog DB), XTDB, Crux, Datascript (in-memory datalog).
- Async: core.async, manifold (Aleph), promesa.
- Testing: clojure.test (built-in), Midje, Kaocha (runner), test.check (property-based via
spec), Eftest. - Validation: spec, Malli, schema (older).
- Build: tools.build + deps.edn, Leiningen, boot (legacy).
- Docs: codox or cljdoc; published to https://cljdoc.org.
- Notable users: Nubank (largest Clojure shop globally), Walmart, CircleCI, Apple, Funding Circle, Atlassian, Citi, Roam Research, Metabase, JUXT, Adgoji.
Gotchas
- Lazy seqs + side effects —
(map println xs)does nothing if not realized. Usedoseqorrun!for side effects,doall/dorunto force. - Lazy seqs + resources — A
(line-seq (reader f))returned from(with-open [r (reader f)] ...)reads from a closed handle. Realize inside thewith-open. - No JVM tail-call optimization —
recuris required for tail calls; mutual recursion needstrampoline. - Equality:
=is value equality (deep, structural);==numeric equality across types;identical?reference equality.=on Java collections boxes Numbers — surprising int/long mixes. contains?on vectors — checks for index presence, not value ((contains? [10 20] 10)returnsfalse!). Usesomeinstead.- AOT compilation in libraries — bakes class files; downstream consumers can’t update. Generally don’t AOT libs.
requiredoesn’t reload — use(require ... :reload)ortools.namespacefor cleaner resets.- Macros referencing namespace-local symbols without resolving (
'foovs`foo) break when used from another ns. Always syntax-quote. reducewith no init on empty coll errors; provide an initial value.shadow-cljsvs JVM Clojure macros: macros must run on JVM in CLJS builds; mind:require-macrosand.cljcreader conditionals.- Reflection warnings silently degrade perf 10-100x; turn on
(set! *warn-on-reflection* true). - Number autoboxing in tight loops — type-hint primitives (
^long) and useunchecked-addif appropriate.
Citations
- Reference: https://clojure.org/reference
- Getting Started: https://clojure.org/guides/getting_started
- Downloads: https://clojure.org/community/downloads
- API docs (1.12): https://clojure.github.io/clojure/
- Clojure source: https://github.com/clojure/clojure
- ClojureScript: https://clojurescript.org/
- ClojureCLR: https://github.com/clojure/clojure-clr
- spec.alpha: https://clojure.org/guides/spec
- core.async: https://github.com/clojure/core.async
- Hickey papers: https://github.com/matthiasn/talk-transcripts
- Style guide (community): https://guide.clojure.style/
- clj-kondo: https://github.com/clj-kondo/clj-kondo
- HAMT (Bagwell): https://lampwww.epfl.ch/papers/idealhashtrees.pdf
- cljdoc: https://cljdoc.org/