~ / track B / clojure advanced

Type hints and performance

Advanced

Clojure 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

  • definline emits an inline function — cheaper than a regular call, but fragile. Use rarely.
  • proxy creates a Java subclass at runtime — slow, useful for tests.
  • gen-class ahead-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:

  1. Profile (Criterium, async-profiler).
  2. Find the hot path.
  3. Turn on *warn-on-reflection* for that namespace.
  4. Hint to clear the warnings.
  5. If still slow, try primitive args / transients / arrays.
  6. Re-measure.

Try it (informational only)

loading sci
press ⌘/Ctrl-↵ or click ▶ run to evaluate

(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 mattersWhat to do
Web request handlersUsually I/O-bound; rarely needs hints. Focus on connection pools and JSON parser choice
JSON / EDN deserializationUse 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 layerHand-tuned with arrays and primitives
nippy serializationUses ^byte arrays throughout for compact encoding
ClojureScript hot rendering pathsDifferent rules — see Reagent's ^js hint and goog.string
Benchmarking toolingcriterium 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)))
  1. Run it on two long vectors and benchmark with (time (dot xs ys)).
  2. Rewrite it as a primitive-friendly loop/recur over ^longs arrays, with ^long accumulator and (aget).
  3. Benchmark again. Where does the speedup come from — fewer allocations, primitive ops, or removed reflection?
 status: new