~ / track B / clojure advanced
Type hints and performance
AdvancedClojure is dynamically typed, but on the JVM it compiles to typed bytecode.
The compiler will use Java's Object for anything ambiguous, which forces
reflection at every method call. Type hints are metadata that tells the
compiler "this is a String," "this is a long" — eliminating reflection
and unlocking primitive math.
Most code does not need them. Hot loops, low-level math, and Java interop do.
See reflection happen
(set! *warn-on-reflection* true)
(defn first-char [s]
(.charAt s 0))
;; Reflection warning - call to method charAt can't be resolved
The warning fires because the compiler doesn't know s is a String — at
the call site, it generates reflective dispatch. Hint it:
(defn first-char [^String s]
(.charAt s 0))
;; (no warning, direct invokevirtual)
^String is metadata on the parameter. The compiler now compiles
(.charAt s 0) to a direct method call on String. Roughly a 10–100×
speedup on hot paths.
Hinting return types
For interop calls whose return type the compiler can't infer:
(defn ^java.util.Date now [] (java.util.Date.))
Without the return hint, callers that do .getTime on the result will
reflect.
Primitive math
Clojure uses boxed Long and Double by default. For tight numeric
loops, you can use unboxed primitives:
(defn ^long sum-to ^long [^long n]
(loop [i 0 acc 0]
(if (== i n)
acc
(recur (inc i) (+ acc i)))))
^long on a parameter or return means primitive long. The body must
stay in primitive land — adding a BigInt or doing (* a 1.5) re-boxes.
GHC-level performance is possible inside loop/recur for numerics.
Limit: functions can take at most 4 primitive args, and they must be
long or double. Beyond that, use arrays or records.
*warn-on-reflection*
Always turn this on for performance-sensitive namespaces:
(ns myapp.hot-path
(:require ...))
(set! *warn-on-reflection* true)
Compile-time warnings flag every reflective call. The rule: zero warnings in the namespaces you care about.
You can also set it project-wide via *warn-on-reflection* in deps.edn
under :aliases.
def of expensive values
(def regex (re-pattern "...")) ;; computed at load, fine
(def big (delay (compute-big))) ;; computed at first use; see [[futures-promises-delay]]
Avoid recomputing constants. Don't inline (re-pattern ...) inside a hot
function — it'll recompile the regex every call.
transient for batch construction
Persistent collections are O(log32 n) per write — fast, but not free. If you build a collection in one place and never share intermediates, use a transient:
(defn build-set [n]
(persistent!
(reduce conj! (transient #{}) (range n))))
transient!, conj!, assoc!, dissoc!, pop! are O(1) amortized for
local construction. Always end with persistent! to return to immutable
land. Don't share transients across threads.
Internally, [[transducers]] use this pattern to build results without intermediate allocation.
Arrays for true hot paths
Java arrays are unboxed and fast:
(let [^longs xs (long-array 1000)]
(dotimes [i (alength xs)]
(aset xs i (* i 2))))
aset, aget, alength, amap, areduce are the array primitives.
Reach for them when you need numerical kernels or memory-tight buffers —
and not before.
definline, proxy, gen-class — niche
definlineemits an inline function — cheaper than a regular call, but fragile. Use rarely.proxycreates a Java subclass at runtime — slow, useful for tests.gen-classahead-of-time compiles a real Java class with methods you can call from Java. Needed when Java needs to call back into Clojure.
A pragmatic checklist
Before adding hints, measure. Most Clojure code doesn't need them, and adding them everywhere clutters reading. The rule:
- Profile (Criterium, async-profiler).
- Find the hot path.
- Turn on
*warn-on-reflection*for that namespace. - Hint to clear the warnings.
- If still slow, try primitive args / transients / arrays.
- Re-measure.
Try it (informational only)
(Note: the in-browser SCI REPL doesn't surface reflection warnings, but the hinted/unhinted distinction is real on the JVM.)
Real-world
| Where speed matters | What to do |
|---|---|
| Web request handlers | Usually I/O-bound; rarely needs hints. Focus on connection pools and JSON parser choice |
| JSON / EDN deserialization | Use jsonista (Jackson-backed) — its internal hot path is fully hinted |
| Numeric simulation, finance, ML preprocessing | ^long / ^double args, loop/recur, sometimes arrays |
| Datomic / XTDB indexing layer | Hand-tuned with arrays and primitives |
nippy serialization | Uses ^byte arrays throughout for compact encoding |
| ClojureScript hot rendering paths | Different rules — see Reagent's ^js hint and goog.string |
| Benchmarking tooling | criterium for micro-benchmarks; JMH for cross-JVM comparisons |
Check yourself
? quiz
`*warn-on-reflection*` shows a warning on `(.length s)`. Adding which hint silences it?
Exercise
Take this function:
(defn dot [xs ys]
(reduce + (map * xs ys)))
- Run it on two long vectors and benchmark with
(time (dot xs ys)). - Rewrite it as a primitive-friendly
loop/recurover^longsarrays, with^longaccumulator and(aget). - Benchmark again. Where does the speedup come from — fewer allocations, primitive ops, or removed reflection?