`reduce-kv`, `into`, and Collection Helpers

Learn when `reduce-kv`, `into`, `group-by`, `frequencies`, and related helpers make collection code simpler, faster, and more idiomatic.

Collection helper: A function that captures a common transformation pattern so you do not have to rebuild it with lower-level loops or repeated manual reduction.

Clojure collection code stays idiomatic when you choose the helper that matches the shape of the job. reduce-kv, into, group-by, frequencies, and merge-with are powerful not because they are magical, but because they encode common patterns directly.

Reach for the Most Specific Helper First

Many collection problems can be solved with a raw reduce, but that is not always the clearest answer. If the job is:

  • build a collection efficiently
  • transform a map entry by entry
  • group values by a derived key
  • count occurrences
  • merge maps with a conflict rule

then Clojure usually already has a helper that says exactly that.

The question is not “can I solve this with reduce?” It is “what helper makes the intent most obvious?”

reduce-kv Is for Associative Collections

reduce-kv shines when you are iterating over maps or other associative structures and both the key and value matter.

1(defn rename-keys [m]
2  (reduce-kv
3   (fn [acc k v]
4     (assoc acc (name k) v))
5   {}
6   m))

Compared with turning the map into a sequence of pairs first, reduce-kv is often clearer because the shape of the job is explicit: walk key-value entries and accumulate a new result.

Use reduce-kv when:

  • both key and value are important
  • you are deriving a new associative result
  • you want one pass over a map-like structure

into Is the Default Builder

into is often the cleanest way to say “take this source and build that target.”

1(into [] (map inc) [1 2 3 4])
2;; => [2 3 4 5]

Or with sets:

1(into #{} (map keyword) ["dev" "ops" "security"])
2;; => #{:dev :ops :security}

The target collection communicates the intended output type, which is often more readable than a manually written reduction.

Prefer Built-In Helpers for Common Shapes

Some transformations show up so often that writing them from scratch just adds noise.

group-by

1(group-by :status
2          [{:id 1 :status :open}
3           {:id 2 :status :closed}
4           {:id 3 :status :open}])

Use group-by when the result should be a map from derived key to items.

frequencies

1(frequencies [:ok :ok :error :ok :warn])
2;; => {:ok 3, :error 1, :warn 1}

Use frequencies when the job is literally counting occurrences.

merge-with

1(merge-with +
2            {:api 3 :db 1}
3            {:api 2 :queue 4})
4;; => {:api 5, :db 1, :queue 4}

Use merge-with when multiple maps share keys and you have a clear combination rule.

Compose Helpers Instead of Overbuilding

Collection helpers work best when each one carries a clear semantic step.

1(->> events
2     (group-by :service)
3     (reduce-kv
4      (fn [acc service xs]
5        (assoc acc service (count xs)))
6      {}))

This pipeline is readable because each step has a specific role:

  1. group events by service
  2. convert each group into a count map

That is usually better than one massive reducer that hides both steps inside one accumulator function.

Efficiency Still Matters, but Clarity Comes First

Clojure helpers are often good performance choices too, especially when paired with transducers or a well-chosen target collection. But the first win is almost always clarity.

Only drop to lower-level reduction patterns when you have a real reason, such as:

  • avoiding unnecessary intermediate collections in a hot path
  • preserving a particular associative shape efficiently
  • combining multiple passes after profiling has shown it matters

Prematurely replacing expressive helpers with custom reducers often makes the code harder to trust.

A Practical Example

Suppose you want a summary of order totals by region:

1(defn totals-by-region [orders]
2  (->> orders
3       (group-by :region)
4       (reduce-kv
5        (fn [acc region xs]
6          (assoc acc region
7                 (transduce (map :total) + 0 xs)))
8        {})))

This is idiomatic because:

  • the grouping step is obvious
  • the accumulation step is map-aware
  • the final result type is explicit

It reads like the business question itself.

Common Mistakes

  • writing a custom reduce for a job group-by or frequencies already solves directly
  • using reduce-kv when only values matter and plain sequence operations would be simpler
  • forgetting that into communicates the target collection type
  • combining too many transformations into one accumulator function
  • optimizing away readable helpers before profiling shows a real cost

Key Takeaways

  • Use the most specific collection helper that matches the job.
  • reduce-kv is best when both map keys and values matter.
  • into is the default builder when the target collection type should be explicit.
  • Helpers like group-by, frequencies, and merge-with usually say intent better than raw reduction.
  • Favor clarity first, then optimize with evidence.

Ready to Test Your Knowledge?

Loading quiz…
Revised on Thursday, April 23, 2026