Clojure
A modern Lisp for the JVM.
Clojure is a modern Lisp that runs on the Java Virtual Machine (and in JavaScript as ClojureScript). It focuses on immutable data, simple functions and interactive development at a REPL. Companies such as Nubank, one of the world's largest digital banks, run core systems on it.
Why learn Clojure?
Clojure makes data the centre of your program, which suits data-heavy and AI-integration work well.
- Immutable data: safe concurrency without locks.
- REPL-driven: change running code and see results instantly.
- JVM power: use any Java library, including AI SDKs.
Set up and run your first program
Install Java (17+) and the Clojure CLI tools. Start a REPL with clj.
brew install clojure/tools/clojure # or the Linux installer
clj -M hello.clj(def name "Ada")
(println (str "Hello, AI! Welcome to Clojure, " name "."))Syntax and values
Everything is an expression in parentheses with the function first: (+ 1 2). Data literals include vectors [], maps {} and keywords like :name.
(def layers 32)
(def model "llama3.1")
(def gpu {:name "H100" :vram 80})
(println model layers)
(println (:name gpu) (:vram gpu))
(println (/ (* 8.03e9 2) 1e9)) ; 16.06Functions
defn defines functions. They can have several arities and destructure their arguments.
(defn params
([layers] (params layers 4096))
([layers width] (* 12 layers width width)))
(defn describe [{:keys [name vram]}]
(str name " has " vram " GB"))
(println (params 32))
(println (describe {:name "H200" :vram 141}))
(println (map #(* % %) [1 2 3])) ; anonymous functionCollections and sequences
Collections are immutable: assoc and conj return new versions. map, filter and reduce work on every collection, and ->> threads data through steps.
(def vram {"H100" 80 "H200" 141 "RTX 5090" 32})
(->> vram
(filter (fn [[_ gb]] (>= gb 80)))
(map first)
(println))
(println (reduce + (vals vram)))
(println (assoc vram "B200" 180))
(println (conj [1 2 3] 4))Control flow and recursion
if, when, cond and case make decisions. loop and recur give fast loops without growing the stack.
(defn precision [bits]
(case bits
32 "FP32"
16 "FP16"
8 "FP8"
"other"))
(println (precision 8))
(println (cond (> 141 100) "huge" :else "small"))
(println (loop [i 1 acc 0]
(if (> i 10) acc (recur (inc i) (+ acc i))))) ; 55State and concurrency
Clojure manages change with reference types. An atom holds a value that you update atomically with swap!; future runs work on another thread.
(def counter (atom 0))
(dotimes [_ 1000] (swap! counter inc))
(println @counter) ; 1000
(def jobs (doall (for [m ["llama" "qwen" "mistral"]]
(future (Thread/sleep 100) (count m)))))
(println (map deref jobs)) ; all run in parallelAI with Clojure
Clojure accesses the whole Java AI ecosystem (Deep Java Library, LangChain4j, ONNX Runtime) and has its own data science stack in Scicloj. Libpython-clj even lets you call PyTorch and Hugging Face directly from Clojure. Its data-first style suits building prompts, parsing JSON and orchestrating tools.
The essential libraries
Gradient descent from scratch
Fit y = 3x + 0.5 with pure functions and reduce.
(def xs (map #(+ -1.0 (* 2.0 (/ % 99))) (range 100)))
(def ys (map #(+ (* 3 %) 0.5) xs))
(def n (count xs))
(defn step [[w b] _]
(let [errs (map (fn [x y] (- (+ (* w x) b) y)) xs ys)
gw (/ (* 2 (reduce + (map * errs xs))) n)
gb (/ (* 2 (reduce + errs)) n)]
[(- w (* 0.1 gw)) (- b (* 0.1 gb))]))
(println (reduce step [0.0 0.0] (range 500))) ; about [3.0 0.5]Build an AI agent in Clojure
Java's built-in HTTP client plus Cheshire for JSON is all you need. Tools are just functions in a map.
;; deps.edn: {:deps {cheshire/cheshire {:mvn/version "5.13.0"}}}
(require '[cheshire.core :as json])
(import '(java.net URI) '(java.net.http HttpClient HttpRequest HttpRequest$BodyPublishers HttpResponse$BodyHandlers))
(def client (HttpClient/newHttpClient))
(def tools [{:type "function" :function {:name "get_weather" :description "Weather for a city"
:parameters {:type "object" :properties {:city {:type "string"}} :required ["city"]}}}])
(def impls {"get_weather" (fn [{:keys [city]}] (str "It is 24°C and sunny in " city "."))})
(defn chat [messages]
(let [req (-> (HttpRequest/newBuilder (URI. "http://localhost:11434/v1/chat/completions"))
(.header "Content-Type" "application/json")
(.POST (HttpRequest$BodyPublishers/ofString
(json/generate-string {:model "llama3.1" :messages messages :tools tools})))
(.build))]
(-> (.send client req (HttpResponse$BodyHandlers/ofString)) .body (json/parse-string true)
:choices first :message)))
(loop [messages [{:role "user" :content "Should I take an umbrella in Lahore today?"}] n 5]
(let [msg (chat messages)]
(if (and (pos? n) (seq (:tool_calls msg)))
(recur (into (conj messages msg)
(for [c (:tool_calls msg)]
{:role "tool" :tool_call_id (:id c)
:content ((impls (get-in c [:function :name]))
(json/parse-string (get-in c [:function :arguments]) true))}))
(dec n))
(println (:content msg)))))Adding a tool is just adding a schema to tools and a function to impls.
ollama pull llama3.1, and keep it running. The code talks to its OpenAI-compatible API at http://localhost:11434/v1, so the same program also works with vLLM, llama.cpp server or a cloud API by changing the URL, model name and adding an API key.Clojure learning roadmap
Tick items off as you go. Your progress is saved in this browser.
Beginner
Intermediate
Advanced
Projects to build
Clojure cheat sheet
| Task | Syntax |
|---|---|
| Define | (def x 5) |
| Function | (defn add [a b] (+ a b)) |
| Lambda | #(* % 2) |
| Map access | (:name gpu) |
| Update | (assoc m :k 1) |
| Thread | (->> xs (map f) (filter g)) |
Common mistakes
- Lazy sequences may not run side effects until used; wrap them in
doallordoseq. - Parentheses carry meaning: extra ones call a value as a function.
- Startup on the JVM is slow; keep a REPL open while developing.
Where to go next
- Clojure.org — official
- Clojure for the Brave and True — free book
- Scicloj — data science
On TensorCore.si: Lisp volume, Java volume.
Keyboard: [ previous language · ] next language · / search the library