Choose `reduce`, `map`, and `filter` by the shape of the work rather than by habit, and know when to switch to transducers.
map, filter, and reduce are three of the most important sequence-processing tools in Clojure. They are simple on the surface, but the real skill is not memorizing them. It is choosing the right one for the shape of the work.
That choice affects clarity more than performance most of the time. If the code uses the wrong abstraction, the pipeline becomes harder to read even when it still “works.”
map transforms each input element into an output element.
filter keeps only the elements that satisfy some predicate.
reduce accumulates many values into one summary or one growing result.
Those roles sound obvious, but people often overuse reduce because it is the most general. In practice, the more specific function is often clearer.
1(def orders
2 [{:id 1 :state :paid :total 40}
3 {:id 2 :state :draft :total 15}
4 {:id 3 :state :paid :total 60}])
5
6(->> orders
7 (filter #(= :paid (:state %)))
8 (map :total)
9 (reduce + 0))
This pipeline reads directly:
Each function does one kind of work. The sequence of transformations is easy to explain.
reduce Is The Right ToolUse reduce when the output is not another plain sequence of the same kind. Good examples:
1(defn index-users-by-id [users]
2 (reduce (fn [idx user]
3 (assoc idx (:id user) user))
4 {}
5 users))
This is a good reduce because the result is a map, not another transformed sequence.
reduce Is A Bad FitIf you are using reduce to simulate a simple map or filter, the code usually becomes noisier than necessary.
Bad instinct:
1(reduce (fn [acc x] (conj acc (* x 2))) [] xs)
Clearer:
1(map #(* 2 %) xs)
The second version states the intent directly.
If you build long map and filter pipelines and the intermediate sequences matter for performance or memory, transducers become relevant. But the design lesson comes first:
That order matters. The cleanest first draft is often an ordinary sequence pipeline. Only after that should you ask whether a transducer would improve the runtime behavior enough to justify the extra abstraction.
The first mistake is choosing reduce because it feels more powerful. Power is not the goal; clarity is.
The second mistake is stacking so many small sequence steps that the business idea disappears. A pipeline should reveal the domain logic, not bury it.
The third mistake is optimizing too early with transducers or low-level accumulation when the plain version would already be clear and fast enough.
Ask these when reviewing a sequence pipeline:
reduce?Those questions usually lead to better Clojure than memorizing any one style rule.