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, or Typed 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.

REPLclj 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/scopingdef 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 flowif, 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/Oclojure.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 highlightsclojure.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 toolscriterium (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 + recordsdefprotocol 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 internalscljs.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 Leiningendeps.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 semanticsatom: 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-info for errors; design data first then functions.
  • Expert review focus: laziness escaping a with-open block (returns a closed handle); mutating Java arrays without copying; reflective Java calls (no ^Type hints); rebinding dynamic Vars across thread boundaries (use bound-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. Use doseq or run! for side effects, doall/dorun to force.
  • Lazy seqs + resources — A (line-seq (reader f)) returned from (with-open [r (reader f)] ...) reads from a closed handle. Realize inside the with-open.
  • No JVM tail-call optimizationrecur is required for tail calls; mutual recursion needs trampoline.
  • 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) returns false!). Use some instead.
  • AOT compilation in libraries — bakes class files; downstream consumers can’t update. Generally don’t AOT libs.
  • require doesn’t reload — use (require ... :reload) or tools.namespace for cleaner resets.
  • Macros referencing namespace-local symbols without resolving ('foo vs `foo) break when used from another ns. Always syntax-quote.
  • reduce with no init on empty coll errors; provide an initial value.
  • shadow-cljs vs JVM Clojure macros: macros must run on JVM in CLJS builds; mind :require-macros and .cljc reader 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 use unchecked-add if appropriate.

Citations