Clojure is a hosted Lisp on the JVM (also ClojureScript on JS, ClojureCLR on .NET). The core differences from Java/Python:
Clojure syntax is almost entirely uniform: (operator arg1 arg2 …). All arithmetic, function calls, control flow, and definitions share this shape.
int x = 3 + 4 * 2; String s = "hello" + " world"; boolean b = x > 5 && x < 20;
x = 3 + 4 * 2 s = "hello" + " world" b = x > 5 and x < 20
(def x (+ 3 (* 4 2))) (def s (str "hello" " world")) (def b (and (> x 5) (< x 20)))
42 ; Long 3.14 ; Double 22/7 ; Ratio — exact rational, not float division "hello" ; String (Java String under the hood) :keyword ; Keyword — intern'd named value, cheap equality 'symbol ; Symbol — names a var; not evaluated in quoted position true false ; Booleans nil ; null — also falsy like false; everything else is truthy \a ; Character literal
nil and false are falsy. 0, "", and [] are all truthy — unlike Python.
Clojure ships four persistent (immutable, structurally-shared) core collections. All implement the same sequence abstraction.
[1 2 3] ; Vector — indexed, O(log32 n) lookup (list 1 2 3) ; List — linked, fast prepend, no random access {:a 1 :b 2} ; Map — hash-map by default #{1 2 3} ; Set — hash-set
[] vectorArrayListlist(list)LinkedListcollections.deque{} mapHashMapdict#{} setHashSetset(sorted-map)TreeMapdict (ordered since 3.7)user = {"name": "Ada", "age": 36}
user["email"] = "ada@example.com" # mutates
name = user.get("name", "unknown")
(def user {:name "Ada" :age 36}) ; assoc returns a NEW map — user is unchanged (def user2 (assoc user :email "ada@example.com")) ; keyword as function — preferred idiom (:name user) ; => "Ada" (get user :name "unknown") ; with default ; nested update (update-in user [:address :city] str " (new)")
a, b, *rest = [1, 2, 3, 4] name = user["name"]
; Sequential destructuring (vectors/lists) (let [[a b & rest] [1 2 3 4]] (+ a b)) ; => 3 ; Map destructuring (let [{:keys [name age]} user] (println name age)) ; With defaults (let [{:keys [name role] :or {role "guest"}} user] role) ; Works in fn args too (defn greet [{:keys [name]}] (str "Hello, " name))
public static int add(int a, int b) { return a + b; } // Variadic public static int sum(int... nums) { return Arrays.stream(nums).sum(); }
def add(a, b): return a + b def sum_all(*nums): return sum(nums)
(defn add [a b] (+ a b)) ; Variadic — rest args collected into a seq (defn sum-all [& nums] (apply + nums)) ; Multi-arity (defn greet ([name] (greet name "Hello")) ([name greeting] (str greeting ", " name))) ; Anonymous fn — two syntaxes (filter (fn [x] (> x 2)) [1 2 3 4]) (filter #(> % 2) [1 2 3 4]) ; #() reader macro, % = first arg
; if — single consequent + alternate (if (> x 0) "positive" "non-positive") ; when — no else branch, implicit do (when (> x 0) (println "positive") x) ; cond — like else-if chain (cond (< x 0) "negative" (= x 0) "zero" :else "positive") ; case — dispatch on value (constant time) (case status :ok "200" :missing "404" "unknown")
All core Clojure data structures are persistent and immutable. "Modifying" a structure returns a new version that shares structure with the old one — O(log n) via Hash Array Mapped Tries, not O(n) copy.
lst = [1, 2, 3] lst.append(4) # mutates in place lst[0] = 99 # mutates in place
(def v [1 2 3]) (def v2 (conj v 4)) ; v still [1 2 3], v2 is [1 2 3 4] (def v3 (assoc v 0 99)) ; v3 is [99 2 3], v unchanged
When you genuinely need mutation, Clojure provides explicit reference types:
; atom — uncoordinated synchronous change, most common (def counter (atom 0)) (swap! counter inc) ; applies fn atomically (reset! counter 0) ; sets absolute value @counter ; deref — current value ; ref — coordinated change via STM transactions (def account-a (ref 100)) (def account-b (ref 200)) (dosync (alter account-a - 50) (alter account-b + 50)) ; atomic across both refs ; agent — async, off-thread updates (def logger (agent [])) (send logger conj "event")
Clojure's collection functions operate on the sequence abstraction. Any seqable (vectors, lists, maps, sets, strings, Java iterables) works with the same functions.
nums = [1, 2, 3, 4, 5] doubled = list(map(lambda x: x * 2, nums)) evens = list(filter(lambda x: x % 2 == 0, nums)) total = sum(nums) grouped = {k: list(v) for k, v in itertools.groupby(nums, key=lambda x: x % 2)}
(def nums [1 2 3 4 5]) (map #(* % 2) nums) ; => (2 4 6 8 10) lazy seq (filter even? nums) ; => (2 4) (reduce + nums) ; => 15 (group-by even? nums) ; => {false [1 3 5], true [2 4]} ; Transducers — composable, no intermediate collections (transduce (comp (filter even?) (map #(* % 2))) + nums) ; => 12
Replaces deeply nested calls or method chaining:
list.stream()
.filter(x -> x % 2 == 0)
.map(x -> x * 3)
.collect(Collectors.toList());
; ->> threads value as LAST arg (->> nums (filter even?) (map #(* % 3)) (into [])) ; => [6 12] ; -> threads as FIRST arg (useful for map/record ops) (-> user (assoc :role "admin") (dissoc :password) (update :name str/upper-case))
Clojure runs on the JVM. All Java classes are available without imports for java.lang.*; others require :import.
; Static method: (ClassName/method args) (Math/sqrt 16.0) ; => 4.0 (System/currentTimeMillis) ; Instance method: (.method obj args) (def sb (StringBuilder.)) ; constructor — note dot suffix (.append sb "hello") (.toString sb) ; Field access (.length "hello") ; => 5 ; Chained — use doto for side effects on one object (doto (java.util.HashMap.) (.put "a" 1) (.put "b" 2)) ; Import in namespace (ns my.app (:import [java.util Date UUID] [java.io File]))
; Without hint: runtime reflection (slow in hot paths) (defn greet [s] (.toUpperCase s)) ; With hint: direct bytecode method call (defn greet [^String s] (.toUpperCase s))
; reify — anonymous one-off implementation (def runnable (reify Runnable (run [this] (println "running")))) ; proxy — when you need to extend a class (proxy [java.util.TimerTask] [] (run [] (println "tick")))
package com.example.app; import java.util.List; import static java.util.Collections.sort;
from os.path import join import collections as col
(ns com.example.app (:require [clojure.string :as str] [clojure.set :as set] [my.other.ns :refer [specific-fn]]) (:import [java.util Date])) ; Use (str/upper-case "hello") (set/union #{1 2} #{2 3})
{:deps {org.clojure/clojure {:mvn/version "1.12.0"}
metosin/malli {:mvn/version "0.16.4"}
com.github.seancorfield/next.jdbc {:mvn/version "1.3.939"}}
:paths ["src" "resources"]
:aliases
{:dev {:extra-paths ["dev"]
:extra-deps {nrepl/nrepl {:mvn/version "1.3.0"}}}
:test {:extra-paths ["test"]}}}
AtomicInteger counter = new AtomicInteger(0); ExecutorService pool = Executors.newFixedThreadPool(4); Future<int> f = pool.submit(() -> expensiveOp()); int result = f.get();
from concurrent.futures import ThreadPoolExecutor with ThreadPoolExecutor(max_workers=4) as ex: f = ex.submit(expensive_op) result = f.result()
; future — runs on a thread pool, deref blocks (def f (future (expensive-op))) @f ; blocks until done ; pmap — parallel map over collection (pmap expensive-op large-list) ; core.async — CSP channels (like Go channels) (require '[clojure.core.async :as a]) (def ch (a/chan 10)) (a/go (a/>! ch "message")) (a/go (let [msg (a/<! ch)] (println msg)))
(try (/ 1 0) (catch ArithmeticException e (println "caught:" (.getMessage e))) (catch Exception e (throw e)) (finally (println "always runs")))
interface Describable { String describe(); }
(defprotocol Describable (describe [this])) ; Implement for a record (defrecord Dog [name breed] Describable (describe [_] (str name " the " breed))) ; Extend an existing type retroactively (open extension) (extend-protocol Describable String (describe [s] (str "String: " s)))
(defmulti area :shape) ; dispatch on :shape key (defmethod area :circle [{:keys [r]}] (* Math/PI r r)) (defmethod area :rect [{:keys [w h]}] (* w h)) (area {:shape :circle :r 5}) ; => 78.54...
(require '[clojure.spec.alpha :as s]) (s/def ::age (s/and int? #(>= % 0))) (s/def ::name (s/and string? #(seq %))) (s/def ::user (s/keys :req-un [::name ::age])) (s/valid? ::user {:name "Ada" :age 36}) ; => true (s/explain ::user {:name "" :age -1}) ; prints failures ; Instrument a fn — validates args at call time (s/fdef greet :args (s/cat :user ::user) :ret string?)
Clojure macros operate on code-as-data at read time. Unlike Java annotations or Python decorators, they can introduce entirely new control flow.
; Writing a simple unless macro (defmacro unless [test & body] `(when (not ~test) ~@body)) (unless (empty? items) (process! items)) ; macroexpand to debug what a macro produces (macroexpand-1 '(unless false (println "hi")))
->, ->>, when, cond, etc.) cover the vast majority of cases.
x != null / x is not None(some? x) / (nil? x)x instanceof Foo / isinstance(x, Foo)(instance? Foo x)String.format() / f"..."(format "..." args)System.out.println / print()(println ...)for (int i…) / enumerate()(map-indexed f coll)IntStream.range(0,10) / range(10)(range 10)list.get(list.size()-1) / lst[-1](last coll) or (peek v)for / while(loop [x init] (recur ...)){**a, **b} (Python 3.5+)(merge a b) / (merge-with + a b)jshell / python -iclj — nREPL for editor integration