Manipulating Collections with Core Functions

Learn how Clojure's core collection functions differ between sequence transformation, shape-preserving updates, and accumulation so collection pipelines stay clear and predictable.

Clojure feels powerful with collections because the core functions compose so well. But that same flexibility can confuse new readers when they do not notice an important distinction: some functions treat a collection as a sequence of values, while others preserve or update the collection’s structural shape.

That distinction is more useful than memorizing a long list of function names. If you understand whether an operation is transforming values, accumulating a result, or updating a keyed structure, collection code becomes much easier to read.

Three Common Kinds of Collection Work

Most day-to-day collection code falls into one of these buckets:

  • transform each element
  • keep or discard elements
  • accumulate or update a result

The visual below shows a typical flow from source collection to transformation pipeline to final result shape.

    flowchart LR
	    A["Source collection"] --> B["filter / map / remove"]
	    B --> C["sequence of transformed values"]
	    C --> D["into [] / into #{} / into {}"]
	    C --> E["reduce to one result"]

The important idea is that map and filter usually produce a sequence-oriented result. If you care about the final concrete collection shape, you often finish with into, vec, set, or reduce.

Sequence-Oriented Functions

map

map applies a function to each element and returns a lazy sequence.

1(map inc [1 2 3 4])
2;; => (2 3 4 5)
3
4(map :name [{:name "Ada"} {:name "Grace"}])
5;; => ("Ada" "Grace")

That second example is useful because keywords are functions of maps. Clojure code often reads naturally when the mapping function is small and direct.

filter and remove

filter keeps values that match a predicate. remove drops values that match it.

1(filter even? [1 2 3 4 5 6])
2;; => (2 4 6)
3
4(remove nil? [1 nil 2 nil 3])
5;; => (1 2 3)

These are especially effective in ->> pipelines where the transformation story stays linear.

keep

keep is often clearer than map followed by filter some? when a transformation may or may not produce a value.

1(keep #(when (even? %) (* % 10)) [1 2 3 4])
2;; => (20 40)

Accumulating with reduce

reduce is the main tool when the result is not “another sequence of similar things” but one accumulated answer.

1(reduce + [1 2 3 4])
2;; => 10
3
4(reduce max [4 9 2 8])
5;; => 9

It is also useful when you want to build a result with explicit control:

1(reduce
2  (fn [acc user]
3    (assoc acc (:id user) (select-keys user [:name :team])))
4  {}
5  [{:id 1 :name "Ada" :team "platform" :active? true}
6   {:id 2 :name "Linus" :team "kernel" :active? false}])
7;; => {1 {:name "Ada", :team "platform"}
8;;     2 {:name "Linus", :team "kernel"}}

If the result is a single answer or a deliberately built structure, reduce is usually the right mental model.

Shape-Preserving Update Functions

Not all collection work is sequence processing. Functions such as assoc, update, dissoc, and conj operate more like structural edits.

assoc and update

Use assoc when you already know the new value, and update when you want to transform an existing value in place conceptually.

1(assoc {:name "Ada"} :role :admin)
2;; => {:name "Ada", :role :admin}
3
4(update {:count 3} :count inc)
5;; => {:count 4}

For nested data, assoc-in and update-in keep the intent readable:

1(update-in {:request {:retries 2}} [:request :retries] inc)
2;; => {:request {:retries 3}}

conj Depends on the Collection Type

conj is powerful precisely because it is polymorphic, but that also means you need to remember that it preserves the collection’s natural insertion semantics.

 1(conj [1 2] 3)
 2;; => [1 2 3]
 3
 4(conj '(1 2) 0)
 5;; => (0 1 2)
 6
 7(conj #{:read} :write)
 8;; => #{:read :write}
 9
10(conj {:name "Ada"} [:role :admin])
11;; => {:name "Ada", :role :admin}

This is one of the most important collection lessons in Clojure: conj is not “append.” It means “add in the way that is natural for this collection.”

Returning the Shape You Actually Want

Many transformation functions return sequences. That is often fine, but sometimes the consumer expects a vector, set, or map.

 1(->> [1 2 3 4 5]
 2     (filter odd?)
 3     (map #(* % %))
 4     (into []))
 5;; => [1 9 25]
 6
 7(->> ["admin" "editor" "admin"]
 8     (map keyword)
 9     (into #{}))
10;; => #{:admin :editor}

The pipeline stays expressive, while into makes the final result explicit.

A Practical Heuristic

When you read or write collection code, ask:

  • am I transforming each value?
  • am I deciding which values survive?
  • am I accumulating one answer?
  • am I editing a keyed structure?
  • do I care about the final collection shape?

Those questions usually point you toward the right function family faster than memorizing isolated examples.

Common Mistakes

Forgetting That map Is Lazy

If a map pipeline contains side effects, the real execution point may be wherever the sequence is consumed, not where the pipeline is defined.

Assuming conj Means the Same Thing Everywhere

It does not. On vectors it feels like append. On lists it adds at the front. On maps it adds entries. On sets it adds a member if it is not already present.

Using reduce for Everything

reduce is powerful, but it is not always the clearest tool. If map plus filter plus into expresses the intent more directly, prefer that.

Forgetting to Reify the Final Shape

If the next layer expects a vector but your pipeline still returns a lazy sequence, make the conversion explicit instead of assuming the consumer will not care.

Design Review Question

A team has this pipeline:

1(->> users
2     (filter :active?)
3     (map :email))

The next function expects a vector because it indexes into the result. What is the cleaner fix?

The cleaner fix is to preserve the readable pipeline and make the final shape explicit:

1(->> users
2     (filter :active?)
3     (map :email)
4     (into []))

That communicates both the transformation and the concrete result contract.

Key Takeaways

  • map, filter, and keep are sequence-oriented transformation tools
  • reduce is for accumulation and deliberate result construction
  • assoc, update, and related functions perform structural edits
  • conj follows the natural semantics of the target collection
  • into is often the final step that makes the desired result shape explicit
Loading quiz…
Revised on Thursday, April 23, 2026