Coding · Programming languages
Clojure tutorial for beginners
Short answer
Clojure is a dynamic, functional Lisp that runs on the Java Virtual Machine (with ClojureScript for JavaScript). Code is written as lists in parentheses, data is immutable by default, and you build programs by combining small functions over vectors, maps and sets. You install the Clojure CLI, start a REPL with clj, and run a file with clojure -M hello.clj.
What Clojure is and who uses it
Clojure was created by Rich Hickey and first released in 2007. It is a Lisp dialect: every expression is a list whose first element is the function or operator, for example (+ 1 2). It compiles to JVM bytecode, so it can call any Java library directly. ClojureScript compiles the same language to JavaScript for browsers and Node.js.
Clojure is used for back-end web services, data processing pipelines and internal business systems, especially where teams value immutable data and interactive development. Developers usually work inside a REPL (read-eval-print loop) connected to their editor, evaluating one expression at a time while the program runs.
Install Clojure and run Hello, world
Install a Java runtime (JDK 17 or newer is a safe choice), then install the Clojure CLI tools from clojure.org (Homebrew on macOS, an install script on Linux, or the Windows installer). The CLI provides two commands: clj (a REPL with line editing) and clojure (for running programs).
;; hello.clj
(println "Hello, world!")clojure -M hello.clj
# Hello, world!
clj
# user=> (+ 1 2)
# 3Real projects keep their dependencies in a deps.edn file and their code under src/, with one namespace per file. Leiningen (lein) is an older but still popular build tool with a project.clj file.
Values, types and names
Clojure is dynamically typed. def binds a global name (a var) and let binds local names. Values do not change after creation: there is no assignment statement.
| Type | Example | Notes |
|---|---|---|
| Integer | 42 | A 64-bit long; 42N is a BigInt |
| Floating point | 3.14 | A Java double |
| Ratio | (/ 1 3) | Exact fraction 1/3, not 0.333 |
| String | "hello" | A Java String, double quotes only |
| Character | \a | A single character |
| Keyword | :name | Fast, self-evaluating label, often a map key |
| Boolean / nil | true, false, nil | Only false and nil are falsy |
(def pi-approx 3.14159)
(let [r 2
area (* pi-approx r r)]
(println "Area:" area))
;; Area: 12.56636
(str "a" 1 :b) ;; => "a1:b"Control flow
In Clojure everything is an expression that returns a value, including if. if takes exactly one "then" and one "else" form; use when for several side effects without an else branch, and cond for several conditions.
(defn grade [score]
(cond
(>= score 90) "A"
(>= score 80) "B"
:else "C"))
(println (grade 85)) ;; B
(when (pos? 5) (println "positive") :done)
(doseq [x [1 2 3]] (println (* x x))) ;; 1 4 9
(for [x (range 5) :when (odd? x)] (* x 10)) ;; => (10 30)
(loop [i 0 acc 0]
(if (< i 5) (recur (inc i) (+ acc i)) acc)) ;; => 10There is no for loop with a counter that you mutate. doseq runs side effects for each element, for builds a lazy sequence (a list comprehension), and loop/recur expresses iteration without growing the call stack.
Functions
Define named functions with defn. A function can have several arities (different numbers of arguments), and #(...) is a short form for small anonymous functions where % is the argument.
(defn greet
([] (greet "world"))
([name] (str "Hello, " name "!")))
(greet) ;; => "Hello, world!"
(greet "Ana") ;; => "Hello, Ana!"
(defn sum-all [& nums] (reduce + nums))
(sum-all 1 2 3) ;; => 6
(map #(* % %) [1 2 3]) ;; => (1 4 9)
(filter even? (range 10)) ;; => (0 2 4 6 8)Functions are values: you can pass them to map, filter and reduce, return them from other functions, and store them in maps.
Collections: lists, vectors, maps and sets
Clojure's four core collections are immutable and persistent: "changing" one returns a new version that shares most of its structure with the old one, so updates are cheap.
| Collection | Literal | Typical use |
|---|---|---|
| List | '(1 2 3) | Code and sequential data added at the front |
| Vector | [1 2 3] | Indexed sequence, appends at the end |
| Map | {:name "Ana" :age 30} | Key-value records |
| Set | #{:red :green} | Unique values, membership tests |
(def user {:name "Ana" :age 30})
(:name user) ;; => "Ana"
(assoc user :email "[email protected]") ;; new map with :email
(update user :age inc) ;; => {:name "Ana", :age 31}
(conj [1 2] 3) ;; => [1 2 3]
(contains? #{:red :green} :red) ;; => true
(get-in {:a {:b 1}} [:a :b]) ;; => 1
user ;; unchanged: {:name "Ana", :age 30}Signature features: threading macros, atoms and macros
Threading macros make data pipelines read from top to bottom. -> inserts the previous result as the first argument of the next form; ->> inserts it as the last argument.
(->> (range 1 11)
(filter odd?)
(map #(* % %))
(reduce +)) ;; => 165
(def counter (atom 0))
(swap! counter inc)
(swap! counter + 10)
@counter ;; => 11
(defmacro unless [test & body]
`(if ~test nil (do ~@body)))
(unless false (println "runs"))Atoms hold state that can change: swap! applies a function atomically, which makes them safe to use from several threads. Refs and agents cover other concurrency cases. Because code is data (lists), macros can transform code before it is compiled; when, cond and -> are themselves macros.
Error handling
Clojure uses JVM exceptions with try, catch and finally. To attach data to an error, throw ex-info and read the data back with ex-data.
(defn parse-age [s]
(let [n (Long/parseLong s)]
(when (neg? n)
(throw (ex-info "Age must not be negative" {:value n})))
n))
(try
(parse-age "-5")
(catch clojure.lang.ExceptionInfo e
(println (ex-message e) (ex-data e)))
(catch NumberFormatException _
(println "not a number")))
;; Age must not be negative {:value -5}A small complete program: word frequencies
(ns wordcount
(:require [clojure.string :as str]))
(defn word-counts [text]
(->> (str/split (str/lower-case text) #"\W+")
(remove str/blank?)
frequencies
(sort-by val >)))
(doseq [[word n] (take 2 (word-counts "The cat and the hat and the bat"))]
(println word n))
;; the 3
;; and 2The program lower-cases the text, splits it on non-word characters with a regular expression literal #"\W+", counts each word with frequencies (which returns a map), and sorts the entries by count in descending order.
Where to go next
- The official guides at clojure.org: Getting Started, Learn Clojure and the deps.edn guide.
- The Clojure reference pages for the reader syntax, special forms and data structures.
- Set up an editor with a connected REPL (for example Calva for VS Code, CIDER for Emacs or Cursive for IntelliJ).
- Try ClojureScript if you want to target the browser.
FAQ
Is Clojure a functional language?
Yes. Clojure favors pure functions and immutable data, but it is pragmatic: it allows side effects and offers managed mutable state through atoms, refs and agents.
Do I need to know Java to learn Clojure?
No. You can learn the language without Java, but you need a Java runtime installed, and knowing a little about the JVM helps when you read stack traces or use Java libraries.
What is the difference between a list and a vector in Clojure?
A list '(1 2 3) adds elements at the front and is mainly used for code; a vector [1 2 3] adds at the end and supports fast access by index. Most data in Clojure programs is stored in vectors and maps.
Why does Clojure use so many parentheses?
Clojure code is written as Lisp data structures, so every call is a list. This uniform syntax is what makes macros possible, and editors with structural editing make the parentheses easy to manage.
What is the difference between Clojure and ClojureScript?
They are the same language on different hosts: Clojure runs on the JVM, ClojureScript compiles to JavaScript. Host interop differs, but core functions and data structures are the same.