Why Threading Macros Exist
Deeply nested function calls like (reduce + (filter even? (map inc coll))) read inside-out, which is hard to follow because execution order runs opposite to reading order. Threading macros rewrite that nesting into a linear top-to-bottom or left-to-right pipeline that reads in the actual order operations happen, without changing what code executes — they're a purely syntactic (macro-level) transformation performed at compile time.
Cricket analogy: Reading a scorecard line like 'caught Kohli, bowled by Bumrah, off a yorker, in the 49th over' backwards would be confusing — threading macros reorder the sentence of function calls into the natural, chronological reading order, like a properly written commentary line.
The Thread-First Macro: ->
-> (thread-first) takes a value and threads it through a series of forms, inserting it as the first argument of each subsequent form; it's the natural choice for functions that take 'the thing being operated on' as their first parameter, like most map, assoc, and update calls on a single data structure. (-> m (assoc :a 1) (update :b inc)) expands to (update (assoc m :a 1) :b inc).
Cricket analogy: A batsman's innings threaded through 'start at crease -> face first ball -> get to fifty -> get to century,' where each stage takes the current innings-state as its first input, mirrors thread-first's data-as-first-arg pattern.
(-> {:a 1 :b 2}
(assoc :c 3)
(update :a inc)
(dissoc :b))
;=> {:a 2 :c 3}Thread-Last (->>) and the Short-Circuiting some-> / some->>
->> (thread-last) inserts the threaded value as the last argument instead of the first, which fits sequence functions like map, filter, and reduce where the collection is conventionally the final parameter; mixing -> and ->> incorrectly is a very common Clojure bug, since assoc expects the map first but filter expects the collection last. some-> and some->> thread the value through the same way but short-circuit to nil the moment any step returns nil, avoiding a chain of errors when data might be missing.
Cricket analogy: A DRS review chain that stops immediately at 'ball tracking shows missing leg stump' without checking the remaining checks is exactly some->'s short-circuit-on-nil behavior.
(->> [1 2 3 4 5]
(filter odd?)
(map inc)
(reduce +))
;=> 12 ; odd? -> (1 3 5), inc -> (2 4 6), sum -> 12
(some-> {:user {:profile nil}}
:user
:profile
:email) ;=> nil, chain stops safely at :profileas-> lets you bind the threaded value to a chosen symbol so it can be placed in any argument position, not just first or last: (as-> 5 x (+ x 1) (str "total: " x)) threads x wherever it appears in each form.
- -> (thread-first) inserts the threaded value as the first argument of each subsequent form.
- ->> (thread-last) inserts the threaded value as the last argument of each subsequent form.
- Threading macros are purely syntactic rewrites performed at macro-expansion time, not runtime behavior changes.
- Mixing up -> and ->> for functions with the wrong argument position is a very common Clojure bug.
- some-> and some->> short-circuit to nil the moment any step in the chain returns nil.
- as-> allows binding the threaded value to a named symbol so it can be placed in any argument position.
- Threading macros greatly improve readability of multi-step transformations compared to deeply nested calls.
Practice what you learned
1. What does (-> {:a 1} (assoc :b 2) (update :a inc)) expand to?
2. Why is ->> typically used with map, filter, and reduce rather than ->?
3. What does (some-> {:user nil} :user :profile :email) return?
4. What is the main advantage of as-> over -> and ->>?
5. Threading macros are best described as:
Was this page helpful?
You May Also Like
Higher-Order Functions in Clojure
Learn how Clojure treats functions as first-class values, letting you pass them as arguments, return them from other functions, and compose new behavior with comp, partial, and complement.
map, filter, reduce in Clojure
Master the three foundational sequence-processing functions that let you transform, select, and accumulate data declaratively instead of writing explicit loops.
Lazy Sequences
Understand how Clojure's lazy sequences defer computation until it's needed, enabling infinite sequences, and learn the chunking and memory pitfalls that trip up beginners.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics