Clojure transducers: single-pass pipeline composition without intermediate sequences
Transducers compose reducing functions into a single-pass pipeline, eliminating intermediate sequences for large or infinite Clojure data.
06 Oct 2026, 00:47 UTC

When you chain map, filter, and other sequence operators over a large dataset, each step builds a lazy sequence that must realize before the next one begins. For large or infinite data, this pattern creates intermediate sequences on the heap, increasing garbage-collection pressure and often requiring multiple passes through the source.
Transducers compose reducing functions that process one element at a time and accumulate a result into a single-pass pipeline, eliminating intermediate sequences for large or infinite data, reducing garbage pressure and enabling efficient composition without materializing intermediate collections.
How transducers compose via comp
Transducers abstract the reducing function's arity. A transducer is a function that takes a reducing function (init + step) and returns a new reducing function. The core arity pattern is (fn [result x] ...): one argument for the accumulated result, one for the input element. This lets comp chain transducers in right-to-left order, producing a single reducing function that processes data in one pass.
(def map-inc-xf (fn [result x] (conj result (inc x)))) (def filter-even-xf (fn [result x] (if (even? x) (conj result x) result))) (def my-pipeline (comp filter-even-xf map-inc-xf)) (into [] my-pipeline (range 10))Using transducers with core functions and into
Clojure's core library provides transducer-aware versions of common operators. Passing a transducer as the first argument to map, filter, or mapcat reuses the same reducing-function arity:
(map map-inc-xf (range 10)) (into [] (comp filter-even-xf map-inc-xf) (range 10))Trade-offs and practical limits
- Opaque stack traces when a transducer pipeline slows down, making the call chain harder to inspect without explicit instrumentation such as println inside the reducing function or clojure.trace.
- Function compatibility: not every Clojure function has been upgraded to accept a transducer as the first argument; legacy code may require wrapping via sequence or explicit reduce calls.
- Over-abstraction: in small scripts or one-off data jobs, a hand-coded loop or comprehension syntax can be more readable than setting up transducer arities.
Getting started: a practical check
To verify that a transducer pipeline delivers the expected output without intermediate allocation, you can compare its result against a hand-coded loop and profile allocation with criterium (a Clojure benchmarking library):
(defn hand-loop [coll] (reduce (fn [acc x] (if (even? x) (conj acc (inc x)) acc)) [] coll)) (def my-pipeline (comp filter-even-xf map-inc-xf)) (into [] my-pipeline (range 10)) (criterium/run-benchmark (fn [] (into [] my-pipeline (range 1000000))))Checking that the output matches the hand-coded loop and observing reduced allocation confirms the transducer benefit. Start by replacing one sequence operator with its transducer equivalent in your next pipeline and measure the difference.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.