Skip to content
Volume C · The TensorCore.si Library

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.

Prologue

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.
Prologue

Set up and run your first program

Install Java (17+) and the Clojure CLI tools. Start a REPL with clj.

terminal
brew install clojure/tools/clojure   # or the Linux installer
clj -M hello.clj
hello.clj
(def name "Ada")
(println (str "Hello, AI! Welcome to Clojure, " name "."))
TipCode is written as lists: the function comes first inside the parentheses.
Chapter 1

Syntax and values

Everything is an expression in parentheses with the function first: (+ 1 2). Data literals include vectors [], maps {} and keywords like :name.

basics.clj
(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.06
Chapter 2

Functions

defn defines functions. They can have several arities and destructure their arguments.

functions.clj
(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 function
Chapter 3

Collections and sequences

Collections are immutable: assoc and conj return new versions. map, filter and reduce work on every collection, and ->> threads data through steps.

collections.clj
(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))
Chapter 4

Control flow and recursion

if, when, cond and case make decisions. loop and recur give fast loops without growing the stack.

control.clj
(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)))))   ; 55
Chapter 5

State 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.

state.clj
(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 parallel
Part II · AI with Clojure

AI 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

libpython-cljCall Python libraries (PyTorch, transformers).
Scicloj (tech.ml.dataset)Fast dataframes and ML.
hato / clj-httpHTTP clients for LLM APIs.
cheshireJSON encoding and decoding.

Gradient descent from scratch

Fit y = 3x + 0.5 with pure functions and reduce.

gradient.clj
(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]
Part III · Build an AI agent

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.

Your goal→Model plans→Tool call→Your code runs the tool→Result back to model→Final answer
agent.clj
;; 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.

Run it locallyInstall Ollama, run 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.
Part IV · Your path

Clojure learning roadmap

Tick items off as you go. Your progress is saved in this browser.

Beginner

Intermediate

Advanced

Part IV · Your path

Projects to build

Word counterCount words in a file with frequencies.
Web APIA REST service with Ring and Reitit.
AI pipelineClean data and summarise it with an LLM.
Reference

Clojure cheat sheet

TaskSyntax
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))
Reference

Common mistakes

  • Lazy sequences may not run side effects until used; wrap them in doall or doseq.
  • Parentheses carry meaning: extra ones call a value as a function.
  • Startup on the JVM is slow; keep a REPL open while developing.
Reference

Where to go next

On TensorCore.si: Lisp volume, Java volume.

Keyboard: [ previous language · ] next language · / search the library