Reading the Parentheses
Hello, World — and where the parenthesis goes
The parentheses are not decoration and there are not more of them than you are used to — they simply moved. Every call is a list whose first element is the thing being called.
fun main() {
println("Hello, World!")
println(1 + 2 * 3)
} ;; The open paren moves to the LEFT of the function name.
;; println("x") becomes (println "x")
(println "Hello, World!")
;; Operators are ordinary functions, so they go first too. There is no operator
;; precedence to remember, because there are no operators.
(println (+ 1 (* 2 3)))
;; Which means + takes any number of arguments.
(println (+ 1 2 3 4 5)) Kotlin's
println("x") and Clojure's (println "x") contain exactly the same tokens; only the paren slid left. Once the function name is inside the parentheses, arithmetic has to be prefix too — and the compensation is real: there is no precedence table, and (+ 1 2 3 4 5) works because + is a variadic function rather than a binary operator. The cost is that arithmetic-heavy code reads worse than it does in Kotlin. The benefit is that the language has one syntactic form, which is the precondition for macros.There are no variables
val maximum = 100 // immutable
var counter = 0 // mutable
fun main() {
counter += 1
val doubled = counter * 2
val message = "counter is $doubled"
println("$maximum $message")
} ;; def creates a namespace-level binding. It is not a variable: nothing here
;; will reassign it.
(def maximum 100)
;; let binds locally. The bindings are a VECTOR of name/value pairs, and each
;; one can see the ones before it.
(let [counter 1
doubled (* counter 2)
message (str "counter is " doubled)]
(println maximum message))
;; The binding is scoped to the let body and does not leak.
;; (println doubled) ← would be an error: unresolved symbol There is no
var, and no counter += 1 — a name is bound once to a value, and if you want a different value you bind a different name. let is the workhorse: it is Kotlin's block-scoped val, written as a vector of pairs, with each binding visible to the next. The habit that has to go is the accumulator loop; the habits that replace it (reduce, recur, and rebinding in a new let) are in the Control Flow section. Real, coordinated mutation does exist — see Atoms — but it is a deliberate, named thing rather than the default.No types, anywhere
Every guarantee Kotlin's compiler gives you is gone: no type checker, no null safety, no exhaustiveness, no arity checking of a function you passed by name. What you get back is a language with almost no ceremony, and a REPL you develop inside of.
fun describe(value: Any?): String = when (value) {
is Int -> "int $value"
is String -> "string of ${value.length}"
null -> "nothing"
else -> "something else"
}
fun main() {
println(describe(7))
println(describe("Kotlin"))
println(describe(null))
// describe(7).length ← compile error, caught before the program runs
} ;; No parameter types, no return type, no annotations. The function accepts
;; whatever it is given and fails when it fails.
(defn describe [value]
(cond
(integer? value) (str "int " value)
(string? value) (str "string of " (count value))
(nil? value) "nothing"
:else "something else"))
(println (describe 7))
(println (describe "Clojure"))
(println (describe nil))
;; A misspelled function is not an error until that line RUNS:
;; (describ 7) ← "Unable to resolve symbol" — at run time.
;; The type checker is your test suite, and the compiler is your REPL.
(println (map describe [1 "two" nil :three])) The predicates (
integer?, string?, nil?) are ordinary functions, and cond is Kotlin's subject-less when — but nothing verifies that you covered every case, and nothing stops a caller passing a map where you expected a number. In exchange, the edit-run loop collapses: Clojure development is done in a live REPL connected to the running program, where you redefine one function and immediately call it. That is a genuinely different workflow, not a slower one, and it is the thing to try before judging the language. When you do want checks, clojure.spec gives you runtime contracts on data shape — richer than a type system in some ways, and enforced only where you ask for it.Data, Keywords & Maps
Maps replace most of your classes
This is the biggest single change to your habits. In Kotlin you declare a type to hold two fields. In Clojure you write a map — and the whole standard library already knows how to work with it.
data class Person(val name: String, val age: Int, val city: String)
fun main() {
val ada = Person("Ada", 36, "London")
println(ada)
println(ada.name)
println(ada.copy(age = 37))
println(ada == Person("Ada", 36, "London"))
} ;; No declaration. The map IS the data.
(def ada {:name "Ada" :age 36 :city "London"})
(println ada)
;; A keyword is a FUNCTION that looks itself up in a map.
(println (:name ada))
(println (get ada :name)) ;; the same thing, spelled out
(println (:nickname ada)) ;; nil — a missing key is not an error
(println (:nickname ada "none")) ;; ...with a default
;; assoc is copy(): it returns a NEW map, sharing structure with the old.
(println (assoc ada :age 37))
(println ada) ;; unchanged
;; update applies a function to one value.
(println (update ada :age inc))
;; Value equality, for free, on every map.
(println (= ada {:name "Ada" :age 36 :city "London"})) Nothing was declared, and yet the map has value equality, a readable printed form, and a
copy() (assoc) — the entire payoff of a data class, with no type. That means every function that works on maps works on your data: merge, select-keys, dissoc, update-in, and every JSON library. The trade is exactly what you would expect: nothing catches (:naem ada), which quietly returns nil instead of failing. Clojure's answer is that shared, generic operations on plain data are worth more than the typo protection, and after a while in a REPL it is hard to argue the point as strongly as you would like to.Keywords: the type Kotlin has no name for
enum class Status { ACTIVE, PAUSED }
fun main() {
// Kotlin's options for "a name used as a value": a String (unsafe, no
// identity) or an enum (safe, but must be declared in advance).
val statuses = mapOf("active" to 1, "paused" to 2)
println(statuses["active"])
println(Status.ACTIVE)
println(Status.valueOf("ACTIVE") == Status.ACTIVE)
} ;; A keyword is an interned, self-evaluating name. No declaration, no enum,
;; and equality is identity — comparing two keywords is a pointer comparison.
(def status :active)
(println status)
(println (= :active status))
(println (keyword "active"))
(println (name :active)) ;; back to a string
;; Keywords are also FUNCTIONS of a map, which is why they are everywhere:
(def counts {:active 1 :paused 2})
(println (:active counts))
(println (map :name [{:name "Ada"} {:name "Alan"}])) ;; a keyword as a mapper
;; And they can be namespaced, which is how you avoid collisions without types.
(println :user/name) A keyword is what Kotlin reaches for an enum or a string to express: a name that is a value. It is interned, so equality is a pointer comparison rather than a string compare; it needs no declaration, so a new one costs nothing; and — the part that makes idiomatic Clojure look the way it does — it is callable, so
(:name person) is a lookup and (map :name people) extracts a field from a collection with no lambda at all. Namespaced keywords (:user/name) are how a large codebase keeps keys from colliding, which is the job a type would otherwise do.nil is safe almost everywhere
Clojure has no null safety in the type system — and needs it far less than you would expect, because most of the standard library treats
nil as an ordinary, harmless value.fun main() {
val person: Map<String, String>? = null
// Every hop needs the operator, because any of them may be null.
println(person?.get("name")?.uppercase() ?: "(none)")
val names: List<String>? = null
println(names?.map { it.uppercase() } ?: emptyList<String>())
println(names?.size ?: 0)
} ;; nil "punning": these all return something sensible instead of blowing up.
(println (:name nil)) ;; nil — a lookup in nothing is nothing
(println (count nil)) ;; 0
(println (map inc nil)) ;; () — an empty sequence
(println (first nil)) ;; nil
(println (conj nil 1)) ;; (1)
;; So the safe-call operator is not needed for most chains.
(def person nil)
(println (clojure.string/upper-case (or (:name person) "(none)")))
;; 'or' is the Elvis operator, and nil/false are the ONLY falsy values.
(println (or nil "fallback"))
(println (or 0 "fallback")) ;; 0 — zero and "" are TRUTHY
(println (some? nil)) This is why Clojure code has so little defensive plumbing:
(:name nil) is nil, (count nil) is 0, (map f nil) is empty, so a chain of lookups over missing data flows through instead of exploding. or is your Elvis operator, and note the falsy rule — only nil and false are false, so 0 and "" are truthy, which is the opposite of the mistake a JavaScript refugee makes. What nil punning cannot save you from is Java interop: call a method on nil and you get the NullPointerException Kotlin was protecting you from all along. It is a library convention, not a guarantee.Functions
defn, and arity instead of default arguments
fun greet(name: String, salutation: String = "Hello") =
"$salutation, $name!"
fun main() {
println(greet("Ada"))
println(greet("Ada", "Hi"))
} ;; The parameter list is a VECTOR. The docstring is part of the definition.
(defn greet
"Greets someone, with an optional salutation."
([name] (greet name "Hello")) ;; one arity delegating to the other
([name salutation] (str salutation ", " name "!")))
(println (greet "Ada"))
(println (greet "Ada" "Hi"))
;; There are no named arguments either — but destructuring a map in the
;; parameter list gets you the same call site, and is the idiom.
(defn make-button [{:keys [label enabled width]
:or {enabled true width 100}}]
(str label " enabled=" enabled " width=" width))
(println (make-button {:label "Save"}))
(println (make-button {:label "Save" :width 200})) Default arguments become multiple arities — a function is a set of implementations chosen by argument count, and the short one usually calls the long one, which is the same telescoping pattern Java forces but written in one form. Named arguments do not exist at all, and the idiomatic replacement is better than it sounds: take a single map and destructure it in the parameter list with
:keys and :or, and the call site reads exactly like Kotlin's named arguments with defaults. That pattern is everywhere in Clojure libraries.Lambdas: fn, #(), and the missing it
fun main() {
val numbers = listOf(1, 2, 3, 4)
println(numbers.map { it * 2 }) // implicit it
println(numbers.filter { it % 2 == 0 })
println(numbers.map { number -> number * 3 }) // named parameter
val add: (Int, Int) -> Int = { first, second -> first + second }
println(add(2, 3))
} (def numbers [1 2 3 4])
;; fn is the full form.
(println (map (fn [number] (* number 2)) numbers))
;; #() is the shorthand. % is the first argument — Kotlin's 'it'.
(println (map #(* % 2) numbers))
(println (filter #(even? %) numbers))
;; %1 %2 for multiple arguments.
(println ((fn [first second] (+ first second)) 2 3))
(println (#(+ %1 %2) 2 3))
;; But most of the time you need no lambda at all: pass the function itself.
(println (map inc numbers))
(println (filter even? numbers))
(println (reduce + numbers)) % is it, and #(…) is the trailing-lambda shorthand — but notice the last block, which is the real lesson. Because everything is a plain function and the library is built out of small ones (inc, even?, +), a huge fraction of Clojure lambdas turn out to be unnecessary: you pass the function by name. The habit to build is to look for an existing function before writing #(…). One rule to know: #() cannot be nested, because there would be no way to say which % you meant.comp, partial, and apply
fun main() {
val numbers = listOf(1, 2, 3)
// Kotlin composes with lambdas; there is no built-in compose or partial.
val doubleThenShow: (Int) -> String = { number -> "[${number * 2}]" }
println(numbers.map(doubleThenShow))
// Partial application is a lambda that closes over the fixed argument.
fun add(first: Int, second: Int) = first + second
val addTen: (Int) -> Int = { number -> add(10, number) }
println(addTen(5))
// "Spreading" a collection into a function: there is no apply, so you reach
// for whichever collection method happens to exist.
println(listOf(3, 1, 4).max())
println(listOf(1, 2, 3).sum())
} (def numbers [1 2 3])
;; comp builds a function from functions, applied RIGHT to left.
(def double-then-show (comp #(str "[" % "]") #(* % 2)))
(println (map double-then-show numbers))
;; partial fixes the leading arguments and returns a new function.
(def add-ten (partial + 10))
(println (add-ten 5))
(println (map (partial * 3) numbers))
;; apply spreads a collection into a function's arguments.
(println (apply max [3 1 4]))
(println (apply + numbers))
;; And these compose with each other, because they are all just values.
(println ((comp (partial * 2) inc) 5)) ;; (5 + 1) * 2 Kotlin can express all of this with lambdas, but it has no
comp, no partial, and no apply in the standard library, so each one is a hand-written closure. Having them as functions matters more than it looks: (partial * 3) is a value you can store in a map, put in a list, and pass on, and comp lets you build a pipeline without naming any of the intermediate steps. Remember that comp runs right to left (like mathematics, unlike a threading macro) — which is exactly why the threading macros in the next section exist.Collections & Threading
Functions on data, not methods on objects
fun main() {
val names = listOf("Ada", "Grace", "Alan", "Barbara")
println(names.filter { it.length > 3 }.map { it.uppercase() })
println(names.sumOf { it.length })
println(names.joinToString(", "))
println(names.groupBy { it.length })
println(names.sortedBy { it.length })
println(names.take(2))
} (def names ["Ada" "Grace" "Alan" "Barbara"])
;; The collection is the LAST argument, not the receiver. There is no dot.
(println (map clojure.string/upper-case (filter #(> (count %) 3) names)))
(println (reduce + (map count names)))
(println (clojure.string/join ", " names))
(println (group-by count names))
(println (sort-by count names))
(println (take 2 names))
;; The same functions work on every collection — vectors, lists, sets, and the
;; key/value pairs of a map. There is no separate API per type.
(println (map key {:a 1 :b 2}))
(println (filter even? #{1 2 3 4}))
(println (into {} (map (fn [[k v]] [k (* v 10)]) {:a 1 :b 2}))) Method chaining is gone, because there are no methods:
map and filter are functions that take the collection as their last argument, which makes the nesting read inside-out (the filter above happens first, despite appearing second). That is unreadable at three levels deep, and the next concept — the threading macro — exists precisely to fix it. What you gain is uniformity: one map works over vectors, lists, sets, maps and lazy sequences, so there is no Iterable/Sequence/Stream split and no method that exists on one type but not another.Threading macros: the method chain, restored
The nesting problem has a solution, and it is the single most-used macro in the language.
fun main() {
val names = listOf("Ada", "Grace", "Alan", "Barbara")
// The dot chain reads left to right, in execution order.
val result = names
.filter { it.length > 3 }
.map { it.uppercase() }
.sorted()
.joinToString(", ")
println(result)
val person = mapOf("name" to "Ada")
println(person["name"]?.uppercase()?.take(2))
} (def names ["Ada" "Grace" "Alan" "Barbara"])
;; ->> threads the value as the LAST argument of each form. Read it top to
;; bottom: it is the dot chain, and it expands to the nested calls for you.
(println
(->> names
(filter #(> (count %) 3))
(map clojure.string/upper-case)
sort
(clojure.string/join ", ")))
;; -> threads as the FIRST argument — which is what map/assoc/update want.
(def ada {:name "Ada" :age 36})
(println
(-> ada
(assoc :city "London")
(update :age inc)
(dissoc :name)))
;; some-> stops at the first nil: this is the safe-call chain.
(println (some-> {:name "Ada"} :name clojure.string/upper-case (subs 0 2)))
(println (some-> {} :name clojure.string/upper-case)) ;; nil, no error The threading macros restore left-to-right reading order, and they are the closest thing Clojure has to Kotlin's dot chain. Two of them, because argument position matters:
->> puts the value last (which is where the sequence functions want it) and -> puts it first (which is where the map functions want it). some-> short-circuits on nil and is a direct replacement for a ?. chain. And here is the part worth pausing on: none of these are language features. They are ordinary macros, defined in the standard library in a few lines, which anyone could have written — and that is the argument for the Macros section below.Every collection is persistent
fun main() {
val readOnly = listOf(1, 2, 3) // read-only VIEW, not immutable
val mutable = mutableListOf(1, 2, 3)
mutable.add(4)
println(mutable)
val scores = mutableMapOf("a" to 1)
scores["b"] = 2
println(scores)
// The nested update is the painful one:
val nested = mutableMapOf("user" to mutableMapOf("age" to 36))
(nested["user"] as MutableMap<String, Int>)["age"] = 37
println(nested)
} ;; Nothing is mutable. conj/assoc return a NEW collection that shares structure
;; with the old one — O(log32 n), not a copy.
(def numbers [1 2 3])
(println (conj numbers 4))
(println numbers) ;; unchanged, and still safe to share
(def scores {:a 1})
(println (assoc scores :b 2))
(println (dissoc (assoc scores :b 2) :a))
;; Nested updates are a single call — the reason update-in is beloved.
(def nested {:user {:name "Ada" :age 36}})
(println (update-in nested [:user :age] inc))
(println (assoc-in nested [:user :city] "London"))
(println (get-in nested [:user :name]))
;; into pours one collection into another.
(println (into #{} [1 2 2 3]))
(println (into [] {:a 1 :b 2})) Immutability here is not a discipline you maintain but the substrate you stand on:
conj and assoc return new collections that share structure with the originals, so copying a large map to change one key is O(log₃₂ n) rather than O(n), and no two threads can ever see a torn value. That is what makes the concurrency story so simple later on. The everyday payoff, though, is update-in / assoc-in / get-in: reaching into nested data with a path vector is one call, where Kotlin's immutable version needs nested copy() calls and its mutable version needs the cast in the left column.Sequences are lazy by default
fun main() {
val firstTwo = listOf(1, 2, 3, 4, 5)
.asSequence()
.onEach { println("seeing $it") }
.map { it * it }
.take(2)
.toList()
println(firstTwo)
println(generateSequence(1) { it * 2 }.take(6).toList())
} ;; map/filter return LAZY sequences: nothing is computed until something asks.
(def squares
(->> [1 2 3 4 5]
(map (fn [number] (println "seeing" number) (* number number)))
(take 2)))
(println "nothing has run yet")
(println (doall squares)) ;; NOW the trace prints, for two elements only
;; Infinite sequences are ordinary values.
(println (take 6 (iterate #(* 2 %) 1)))
(println (take 5 (range))) ;; range with no argument is infinite
(println (take 5 (repeat :x)))
(println (take 5 (cycle [1 2]))) Laziness is the default rather than an
asSequence() opt-in, so map over a million elements costs nothing until something realizes it — and infinite sequences ((range), (iterate f x), (cycle …)) are ordinary values you can pass around. The trap is the mirror image of Kotlin's: because nothing runs until it is forced, a map whose function has a side effect may run later than you think, in a different context, or never — which is why the example needs doall to force it, and why side-effecting work belongs in doseq or run! rather than map. Lazy sequences also cache, so realizing one twice does the work once.Control Flow & Recursion
if, when, cond, case
fun describe(code: Int): String = when (code) {
200 -> "OK"
301, 302 -> "Redirect"
in 500..599 -> "Server Error"
else -> "Unknown"
}
fun main() {
println(describe(200))
println(describe(302))
println(describe(503))
val score = 87
val grade = if (score >= 90) "A" else "B"
println(grade)
} ;; case dispatches on a CONSTANT (fast, hash-based). No ranges, no conditions.
(defn describe [code]
(case code
200 "OK"
(301 302) "Redirect" ;; several constants in one clause
"Unknown")) ;; the trailing form is the default
(println (describe 200))
(println (describe 302))
(println (describe 999))
;; cond takes arbitrary tests — it is Kotlin's subject-less 'when'.
(defn classify [code]
(cond
(= code 200) "OK"
(<= 500 code 599) "Server Error" ;; note: <= takes 3 arguments
:else "Unknown"))
(println (classify 503))
;; if is an expression and takes exactly two branches.
(println (if (>= 87 90) "A" "B"))
;; when = if with no else, and an implicit body of several forms.
(when (> 87 50)
(println "passing")
(println "still passing")) Kotlin's single
when splits into three: case for constant dispatch (fast, but no ranges or guards — and note it does not fall through to cond-style tests), cond for arbitrary conditions, and if for the two-branch expression. when in Clojure is not Kotlin's when — it is an if with no else and a multi-form body, which is a false friend worth burning in early. What is missing is exhaustiveness: nothing checks that your cond covered every case, and a cond that matches nothing returns nil rather than failing.Loops become recur, reduce, and for
There is no mutable loop counter, so the
for loop you know does not exist. Three things replace it, and only one of them is recursion.fun main() {
// The accumulator loop.
var total = 0
for (number in 1..5) total += number
println(total)
// The transform loop.
val doubled = mutableListOf<Int>()
for (number in 1..3) doubled.add(number * 2)
println(doubled)
// The side-effect loop.
for (name in listOf("Ada", "Alan")) println(name)
} ;; 1. The accumulator loop is 'reduce' — and this is what you should reach for.
(println (reduce + (range 1 6)))
(println (reduce (fn [total number] (+ total number)) 0 [1 2 3 4 5]))
;; 2. The transform loop is 'map' — or 'for', which is a COMPREHENSION and
;; returns a sequence (it is not a loop at all, despite the name).
(println (map #(* 2 %) (range 1 4)))
(println (for [number (range 1 4)] (* 2 number)))
(println (for [number (range 1 10) :when (odd? number)] number))
;; 3. The side-effect loop is 'doseq', which returns nil and exists to do things.
(doseq [name ["Ada" "Alan"]]
(println name))
;; And where you really need explicit iteration with state: loop/recur.
;; recur is a TAIL CALL that reuses the stack frame — no overflow.
(println
(loop [number 5
total 1]
(if (zero? number)
total
(recur (dec number) (* total number))))) ;; 5! = 120 Note that Clojure's
for is a false friend twice over: it is not a loop, it does not iterate for effect, and it returns a lazy sequence — it is a list comprehension, with :when and :let modifiers. The loop you actually want is almost always reduce. When you genuinely need explicit iteration with carried state, loop/recur is it: recur rebinds the loop variables and jumps, reusing the stack frame, which is how Clojure gets tail recursion on a JVM that does not have it. Forgetting recur and calling the function by name instead is the classic beginner's stack overflow.Destructuring
Map destructuring with :keys
data class Person(val name: String, val age: Int, val city: String)
fun main() {
val ada = Person("Ada", 36, "London")
// Positional only, and only for data classes.
val (name, age) = ada
println("$name $age")
// For a map there is no destructuring at all.
val person = mapOf("name" to "Grace", "age" to 45)
val personName = person["name"]
println(personName)
} (def ada {:name "Ada" :age 36 :city "London"})
;; :keys pulls out the keys you name, by name — not by position.
(let [{:keys [name age]} ada]
(println name age))
;; :or supplies defaults, and :as keeps the whole map.
(let [{:keys [name city nickname]
:or {nickname "none"}
:as whole} ada]
(println name city nickname (count whole)))
;; It works in a parameter list, which is how idiomatic functions take options.
(defn describe [{:keys [name age]}]
(str name " is " age))
(println (describe ada))
(println (map :name [{:name "Ada"} {:name "Alan"}]))
;; And it nests, following the shape of the data.
(let [{{:keys [age]} :user} {:user {:name "Ada" :age 36}}]
(println age)) Kotlin's destructuring is positional and works only where
componentN exists, so a map — the thing you will be holding constantly in Clojure — cannot be destructured at all. Clojure's is by key, comes with defaults (:or), can keep the whole value (:as), nests to any depth, and works anywhere a binding form appears: in let, in a defn parameter list, in a for, in a doseq. Destructuring a map in the parameter list is the standard way to write a function that takes options, and it is the direct substitute for the named arguments Clojure does not have.Sequential destructuring, with a rest
fun main() {
val numbers = listOf(10, 20, 30, 40)
// Lists cannot be destructured positionally in Kotlin.
val first = numbers.first()
val rest = numbers.drop(1)
println("$first $rest")
val pairs = listOf(1 to "one", 2 to "two")
for ((number, label) in pairs) {
println("$number = $label")
}
} ;; Vectors, lists, and any sequence destructure positionally. & takes the rest.
(let [[first second & rest] [10 20 30 40]]
(println first second rest))
;; It nests, and _ discards.
(let [[_ [inner]] [1 [2 3]]]
(println inner))
;; A map's entries are pairs, so this works over a map too.
(doseq [[key value] {:a 1 :b 2}]
(println key "=" value))
;; Strings destructure as sequences of characters.
(let [[initial] "Ada"]
(println initial)) Positional destructuring works on any sequential thing — a vector, a list, a lazy sequence, a string, a map entry — with
& collecting the rest and _ discarding what you do not need. Kotlin's equivalent works only on types that implement componentN, which in practice means data classes and pairs, so a plain List has to be taken apart with first() and drop(1). Combined with the map destructuring above, this is one uniform binding language that appears in every position that binds a name — and there is only one to learn.Polymorphism
Interfaces become protocols
interface Shape {
fun area(): Double
fun describe(): String = "a shape of area ${area()}"
}
class Circle(val radius: Double) : Shape {
override fun area() = 3.14159 * radius * radius
}
class Rectangle(val width: Double, val height: Double) : Shape {
override fun area() = width * height
}
fun main() {
val shapes: List<Shape> = listOf(Circle(1.0), Rectangle(2.0, 3.0))
shapes.forEach { println(it.describe()) }
} ;; A protocol is a named set of functions — an interface, with no implementation.
(defprotocol Shape
(area [this]))
;; A record is a map that can also implement protocols. It is a data class:
;; value equality, keyword access, assoc — plus type-based dispatch.
(defrecord Circle [radius]
Shape
(area [this] (* 3.14159 radius radius)))
(defrecord Rectangle [width height]
Shape
(area [this] (* width height)))
;; A plain function, defined OUTSIDE the protocol, works on anything implementing it.
(defn describe [shape]
(str "a shape of area " (area shape)))
(def shapes [(->Circle 1.0) (->Rectangle 2.0 3.0)])
(doseq [shape shapes]
(println (describe shape)))
;; A record is still a map.
(println (:radius (->Circle 2.0)))
(println (= (->Circle 1.0) (->Circle 1.0))) A
defrecord is close to a Kotlin data class — value equality, keyword access, assoc — and it can implement protocols, which is where the type-based dispatch comes from. The differences worth knowing: a protocol has no default methods, so describe becomes an ordinary function rather than an interface member (which is arguably cleaner — it can be extended without touching the protocol); and protocols can be extended to types you do not own, including String and nil, with extend-type. That last part is the type-class-shaped hole Kotlin fills with extension functions, except that a protocol dispatches dynamically on the value's actual type, so it works through a collection of mixed types.Multimethods: dispatch on anything you like
The kind of polymorphism Kotlin cannot express. A multimethod chooses its implementation by running a function of the arguments — any function — so the dispatch value can be a field, a computed property, or a combination of all of them.
sealed interface Event
data class Click(val x: Int, val y: Int) : Event
data class KeyPress(val key: String) : Event
// Dispatch is on the TYPE, and only the type. Adding a new Event means editing
// this when — the file that owns it.
fun handle(event: Event): String = when (event) {
is Click -> "click at ${event.x},${event.y}"
is KeyPress -> "key ${event.key}"
}
fun main() {
println(handle(Click(3, 4)))
println(handle(KeyPress("Escape")))
} ;; The dispatch FUNCTION decides what to dispatch on. Here: the :type key.
(defmulti handle :type)
(defmethod handle :click [event]
(str "click at " (:x event) "," (:y event)))
(defmethod handle :key-press [event]
(str "key " (:key event)))
(defmethod handle :default [event]
(str "unknown event " (:type event)))
(println (handle {:type :click :x 3 :y 4}))
(println (handle {:type :key-press :key "Escape"}))
(println (handle {:type :scroll}))
;; A new case can be added from ANY namespace, later, without touching the above.
(defmethod handle :scroll [event]
(str "scrolled " (:amount event)))
(println (handle {:type :scroll :amount 20}))
;; And dispatch is not limited to a type: it can be any function of the args.
(defmulti describe-number (fn [number] (cond (neg? number) :negative
(zero? number) :zero
(even? number) :even
:else :odd)))
(defmethod describe-number :negative [_] "negative")
(defmethod describe-number :zero [_] "zero")
(defmethod describe-number :even [number] (str number " is even"))
(defmethod describe-number :odd [number] (str number " is odd"))
(println (map describe-number [-1 0 4 7])) Kotlin's
when (event) over a sealed hierarchy is closed — the compiler guarantees you handled every case, and the price is that adding a case means editing that when, in the file that owns it. A multimethod is open: the dispatch function can be anything (a keyword, a computed classification, a vector of two argument types for double dispatch), and a new implementation can be added from another namespace, later, by someone who does not control the original. That solves the expression problem in the direction Kotlin cannot, and it gives up exactly what you would expect: nothing tells you a case is missing, and :default is the only safety net.State & Atoms
Mutation is a value in a box
class Counter {
// A mutable field. Two threads touching this need a lock, and the compiler
// will not tell you that.
private var count = 0
fun increment(): Int {
count += 1
return count
}
fun value() = count
}
fun main() {
val counter = Counter()
counter.increment()
counter.increment()
println(counter.value())
} ;; An atom is a mutable REFERENCE to an immutable value. The value never
;; changes; the reference is swapped, atomically.
(def counter (atom 0))
(swap! counter inc) ;; apply a function to the current value
(swap! counter inc)
(swap! counter + 10) ;; extra args are passed to the function
(println @counter) ;; @ derefs: reads the current value
(println (deref counter)) ;; the same thing, spelled out
(reset! counter 0) ;; set it outright
(println @counter)
;; State usually IS a map, and update-in works through the atom.
(def app-state (atom {:users [] :count 0}))
(swap! app-state update :count inc)
(swap! app-state update :users conj {:name "Ada"})
(println @app-state) swap! takes a function, not a value: it reads the current value, applies your function, and compare-and-swaps the result in, retrying if someone else got there first. Because the value inside is immutable, that retry is always safe — there is no half-updated state to observe, no lock to take, and no @Volatile or synchronized to remember. That is the whole concurrency story for most Clojure programs, and it is the direct payoff of persistent data structures. The rule that follows: your function must be pure, since a retry will run it again — a swap! whose function prints or writes to a database is a bug waiting for contention.Nothing to synchronize
import kotlinx.coroutines.*
fun main() = runBlocking {
// A shared mutable list. Two coroutines appending to it is a data race,
// and nothing in the type system says so.
val results = mutableListOf<Int>()
val lock = Any()
coroutineScope {
(1..4).forEach { number ->
launch(Dispatchers.Default) {
val squared = number * number
synchronized(lock) { results.add(squared) } // the lock is on you
}
}
}
println(results.sorted())
} ;; The data cannot be mutated, so it cannot be raced. A shared value is just a
;; value, and passing it anywhere is free — there is nothing to defensively copy.
(def shared [1 2 3 4])
;; Every "modification" produces a new value; 'shared' is untouched forever.
(def bigger (conj shared 5))
(println shared)
(println bigger)
;; Where genuinely shared, coordinated state is needed, an atom gives you
;; lock-free updates — the function is retried, never the data corrupted.
(def results (atom []))
(doseq [number [1 2 3 4]]
(swap! results conj (* number number)))
(println @results)
;; On the JVM this same code parallelizes with (pmap ...) or futures and needs
;; NO locks, because there is no mutable state to protect. This is the argument for the whole paradigm, and it is worth taking seriously: because no collection can be mutated, the concept of a data race over your program's data mostly ceases to exist. There is no
synchronized, no lock ordering, no defensive copy before handing a list to another thread, and no @Volatile. Kotlin gives you excellent tools for managing shared mutable state (coroutines, Mutex, immutable data classes by convention) — but they are tools for a problem Clojure declined to have. What Clojure gives up is in-place performance: a tight numeric loop mutating an array is genuinely faster in Kotlin, and Clojure's answer (transients, primitive arrays, type hints) is an opt-in escape hatch you use in the 1% of code that needs it.Macros: Code Is Data
Adding a language feature, without a compiler release
The headline. Because every Clojure program is written in Clojure's own data structures, a macro is an ordinary function that runs at compile time and takes code as its argument.
// Kotlin can approximate a new control structure with an inline function taking
// a lambda — this is what 'run', 'let', 'apply' and 'synchronized' all are.
inline fun unless(condition: Boolean, body: () -> Unit) {
if (!condition) body()
}
fun main() {
unless(false) { println("ran") }
unless(true) { println("never") }
// But the argument arrives as a VALUE or a lambda — never as source code.
// You cannot write a construct that inspects or rewrites the expression
// it was handed. That requires a compiler plugin.
} ;; A macro receives the unevaluated FORM. The backtick quotes it; ~ unquotes,
;; splicing a piece back in; ~@ splices a list of forms.
(defmacro unless [condition & body]
`(if (not ~condition)
(do ~@body)))
(unless false (println "ran"))
(unless true (println "never"))
;; The code the compiler actually sees:
(println (macroexpand-1 '(unless false (println "ran"))))
;; A macro can INSPECT and REWRITE the expression it was handed, because the
;; expression is just a list. Here: print each step of an expression as it runs.
(defmacro spy [form]
`(let [result# ~form]
(println '~form "=>" result#)
result#))
(spy (+ 1 2))
(spy (map inc [1 2 3])) Look at what
spy did: it received (+ 1 2) as a three-element list, kept a copy to print, and emitted new code that evaluates it and reports it. Kotlin cannot do that at any level — an inline function taking a lambda gets close to a new control structure (which is exactly what run, apply and synchronized are), but the body arrives as a lambda, not as source you can read, take apart, or rewrite. That is the whole difference: in Clojure, features like ->, when, cond, defn itself, and async/await-style syntax (core.async's go block) are library code, written by users, needing no change to the language. In Kotlin each of those is a compiler feature you wait for.Homoiconicity: the program is a list
fun main() {
// Kotlin source is text. At run time the program's structure is gone —
// reflection can see classes and methods, but never the expression
// '1 + 2 * 3' as a manipulable tree.
val expression = "(+ 1 (* 2 3))"
println(expression) // just a String; nothing can evaluate it
println(expression.length)
} ;; ' quotes a form: it is data, not something to evaluate.
(def expression '(+ 1 (* 2 3)))
(println expression)
(println (first expression)) ;; the symbol +
(println (rest expression)) ;; the arguments — it is a LIST
(println (count expression))
;; So ordinary functions can rewrite a program.
(def doubled (cons '+ (map (fn [form] (if (number? form) (* 2 form) form))
(rest expression))))
(println doubled)
;; And eval runs it. This is what a macro does, at compile time.
(println (eval expression))
(println (eval doubled)) This is what "code is data" actually means:
(+ 1 (* 2 3)) is a list whose first element is a symbol, so first, rest, map and count work on your program exactly as they work on any other list. That is the property that makes macros ordinary rather than magical, and it is the reason Lisp has no syntax to speak of — the uniform parenthesized form is the price of admission, and macros are what you buy with it. Kotlin's source is text with a grammar; by run time the tree is gone, and rewriting it means writing a compiler plugin against an internal API. Whether the trade is worth it is the oldest argument in programming, and this page will not settle it — but you should know precisely what is being traded.Errors & Exceptions
try/catch, and ex-info
class NotFoundException(val id: Int) : Exception("no such id: $id")
fun find(id: Int): String {
if (id != 1) throw NotFoundException(id)
return "Ada"
}
fun main() {
val name = try {
find(99)
} catch (error: NotFoundException) {
"(missing #${error.id})"
} finally {
println("done")
}
println(name)
println(runCatching { "nope".toInt() }.getOrElse { -1 })
} ;; ex-info builds an exception carrying a MAP of data — no class to declare.
(defn find-user [id]
(if (= id 1)
"Ada"
(throw (ex-info "not found" {:type :not-found :id id}))))
(println
(try
(find-user 99)
(catch :default error
;; ex-data pulls the map back out, and you dispatch on its contents.
(let [{:keys [type id]} (ex-data error)]
(str "(" (name type) " #" id ")")))
(finally
(println "done"))))
;; try is an expression, so this is runCatching:
(println
(try
(Integer/parseInt "nope")
(catch :default _ -1))) The shape is familiar —
try is an expression, as in Kotlin, and there are no checked exceptions — but ex-info is the idiom worth stealing conceptually. Instead of declaring an exception class per failure mode, you throw a message plus an arbitrary map, and the handler pulls that map back out with ex-data and destructures it. Failure information becomes data you can pattern-match on, filter, log as JSON, or pass across a boundary, rather than a type hierarchy you have to design in advance. The catch clause here is written (catch :default …), which is the ClojureScript catch-all; on the JVM you would write the class name, and that difference is one of the few real gaps between the two runtimes.require becomes :pre and :post
class Rectangle(val width: Int, val height: Int) {
init {
require(width > 0) { "width must be positive" }
require(height > 0) { "height must be positive" }
}
val area: Int get() = width * height
}
fun main() {
println(Rectangle(3, 4).area)
try {
Rectangle(-1, 4)
} catch (error: IllegalArgumentException) {
println("rejected: ${error.message}")
}
} ;; A map of :pre and :post conditions, checked at run time. :post binds the
;; return value to %.
(defn area [width height]
{:pre [(pos? width) (pos? height)]
:post [(pos? %)]}
(* width height))
(println (area 3 4))
;; NOTE the catch: a failed :pre throws an AssertionError, which is an Error and
;; NOT an Exception — so it needs the catch-all, not the usual exception class.
(println
(try
(area -1 4)
(catch js/Error error
(str "rejected: " (ex-message error)))))
;; And the general-purpose runtime check:
(println
(try
(assert (pos? -1) "must be positive")
(catch js/Error error (str "assert: " (ex-message error))))) The
{:pre …} / {:post …} map is a small feature with a nice property: it is part of the function's definition rather than its body, so it reads as a contract, and % in a :post condition is the return value. Unlike Dart's assert, these run in production by default (they can be compiled out with *assert*, but the default is on), so they behave like Kotlin's require. For serious contracts on data shape rather than on a single value, the real tool is clojure.spec: it describes the structure of a map, validates it, explains failures in detail, and generates test data from the same description — a checking story with no Kotlin equivalent, precisely because Kotlin gets some of it from the type system instead.Java Interop
Calling Java, and where the parentheses go
Clojure is a JVM language, and reaching Java is a one-character syntax change. (These examples are not runnable here: the in-browser runtime is ClojureScript, which has no JVM under it.)
import java.time.LocalDate
import java.util.ArrayList
fun main() {
val today = LocalDate.of(2026, 7, 14)
println(today.plusDays(1))
println(today.year)
val list = ArrayList<String>()
list.add("Ada")
println(list)
println("hello".toUpperCase().substring(0, 3))
} (import '[java.time LocalDate]
'[java.util ArrayList])
;; Static method: Class/method
(def today (LocalDate/of 2026 7 14))
;; Instance method: (.method object args) — the dot moves INSIDE the paren.
(println (.plusDays today 1))
(println (.getYear today))
;; Constructor: (Class. args) — note the trailing dot.
(def items (ArrayList.))
(.add items "Ada")
(println items)
;; .. and doto chain, since (.b (.a x)) nests inside-out.
(println (.. "hello" toUpperCase (substring 0 3)))
(println (-> "hello" .toUpperCase (subs 0 3)))
(println (doto (ArrayList.)
(.add "Ada")
(.add "Alan"))) Interop is deliberately unglamorous:
(.method object args), (Class/staticMethod args), (Class. args) for a constructor, and doto when you have to call several mutating methods on the same object. Everything in the JVM ecosystem is reachable, which is the pragmatic argument for Clojure over other Lisps. Two cautions carry over from Kotlin's platform types: Java methods return real null, and the nil-punning that makes Clojure so forgiving does not apply to a method call on nil — that is a NullPointerException, same as ever. And a Java collection is mutable, so wrap it in a Clojure collection ((into [] javaList)) at the boundary rather than passing it around.