~ / track B / clojure advanced

Transducers

Advanced

A transducer is a transformation decoupled from its input and output. The same filter or map logic that you'd normally apply to a vector can be fused into a single pass over a stream, a channel, or any sink — without allocating intermediate collections. Transducers are how Clojure makes "map then filter then take" fast at scale.

The shape of the problem

The familiar ->> pipeline is clear, but each step builds a new lazy seq:

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

For 1,000 items it doesn't matter. For 1,000,000, or for streaming through a channel, each intermediate sequence costs memory and indirection.

Minimal example: build a transducer

When you call map, filter, etc. without a collection, they return a transducer — a recipe — instead of doing the work:

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

comp here reads top-to-bottom: increment, then filter, then take 5. (It's the opposite of comp on plain functions because transducers compose into a nested transformation chain.)

Now apply the same xf to different "contexts":

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

One recipe, three sinks. No intermediate collections were allocated.

Why this matters

  • No intermediate seqs. (map f (filter pred xs)) builds a seq for the filter, then another for the map. A transducer fuses them into one pass.
  • Sink-agnostic. The same xf runs over collections, lazy seqs, channels, reducible sources, even Java streams via interop. You write the pipeline once and reuse it.
  • Early termination is real. take, take-while, etc. signal "done" and the rest of the pipeline stops — important for both performance and infinite sources.

Stateful transducers

Some transducers carry state across items. dedupe only emits values different from the previous one, partition-all buffers groups, etc. They still work fine in a pipeline, but the state is per-application — it's safe to reuse the recipe.

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

Writing one by hand

A transducer is a function xf that takes a "reducing function" and returns another reducing function. You usually don't write them by hand, but seeing the shape helps demystify the API:

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

The three arities are init / completion / step — almost all transducers follow that shape.

Check yourself

? quiz

What is the practical advantage of `(into [] (comp (map inc) (filter even?)) xs)` over `(->> xs (map inc) (filter even?) vec)`?

Exercise

Build a transducer xf that, over (range 100), keeps only multiples of 3, squares them, and stops after the first 4. Apply it with into [] and with transduce + 0 to see both shapes.

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