Working with Sequences and Collections in Clojure

Understand how Clojure's sequence abstraction relates to concrete collection types, when laziness helps, and how to avoid common seq-processing mistakes.

Sequence: A logical view over ordered values that lets many different Clojure collections be processed through one common set of operations.

This topic matters because Clojure code often feels collection-agnostic at first glance. You can map, filter, reduce, and first across many different kinds of data. But if you stop there, you miss the practical distinction between a sequence view and the concrete collection shape underneath it.

That distinction drives both correctness and performance.

Collections Are Concrete; Sequences Are a View

Vectors, lists, maps, and sets are concrete collection types. A sequence is the uniform traversal interface many of them can expose.

1(seq [1 2 3])
2;; => (1 2 3)
3
4(seq #{1 2 3})
5;; => (1 3 2) ; order not guaranteed
6
7(seq {:a 1 :b 2})
8;; => ([:a 1] [:b 2]) ; map entries as seq elements

That means sequence functions often tell you how to walk the values, not what the original collection means.

Why the Distinction Matters

If you process a map as a sequence, you are really processing map entries. If you process a set as a sequence, you are walking values in an order you should not depend on. If you process a vector as a sequence, you are often giving up the fact that indexed access exists.

So the key lesson is:

  • use sequence functions for traversal and transformation
  • remember the concrete collection still determines semantics such as order, uniqueness, keyed lookup, and insertion behavior

Core Sequence Operations

seq

seq converts a collection into a sequence view when possible, or returns nil for emptiness.

1(seq [])
2;; => nil
3
4(seq [1 2 3])
5;; => (1 2 3)

That nil behavior is one reason seq is useful in conditionals.

first, rest, and next

These are the classic sequence accessors.

1(first [10 20 30])
2;; => 10
3
4(rest [10 20 30])
5;; => (20 30)

next is slightly stricter than rest: it returns nil instead of an empty sequence when nothing remains. That difference matters in some recursive or conditional flows.

cons

cons adds one item to the front of a sequence view.

1(cons 0 [1 2 3])
2;; => (0 1 2 3)

Notice the result is a sequence, not “the same collection type with one extra element.” That detail surprises people who expect vector semantics.

Lazy Sequence Pipelines

Many sequence-producing functions are lazy:

  • map
  • filter
  • remove
  • take
  • drop
  • repeat
  • iterate
1(def transformed
2  (->> (range)
3       (map #(* % %))
4       (filter odd?)))
5
6(take 5 transformed)
7;; => (1 9 25 49 81)

Laziness is powerful because it lets you describe pipelines without realizing everything immediately. But it also means evaluation happens at the point of consumption, not where the pipeline is declared.

Concrete Result Shape Still Matters

A sequence pipeline often returns a sequence, not necessarily the concrete collection type the next layer expects.

1(->> [1 2 3 4 5]
2     (filter odd?)
3     (map #(* % 10))
4     (into []))
5;; => [10 30 50]

That final into [] is not cosmetic. It communicates that the output should be a vector, not just a lazy seq.

This is a recurring Clojure pattern:

  • sequence operations describe transformation
  • into, vec, set, or a reducing step choose the final shape

Maps and Sets Need Extra Attention

Maps

When you seq a map, you get map entries.

1(map first {:a 1 :b 2})
2;; => (:a :b)

That can be useful, but only if you remember what is happening. If your real goal is to transform values while preserving keyed structure, functions such as update, reduce-kv, or explicit into {} may fit better.

Sets

Sets expose sequence behavior, but they do not promise stable order.

1(seq #{:a :b :c})

If later code relies on order, the bug is not in the sequence API. The bug is treating a set like an ordered collection.

Laziness and Side Effects

Because sequence pipelines are often lazy, embedding side effects inside them can confuse the execution point.

1(def xs
2  (map #(do (println "processing" %) (* % 2))
3       [1 2 3]))

That print does not necessarily happen where the pipeline is defined. It happens where the sequence is consumed.

If the real goal is effects, prefer:

  • doseq
  • run!
  • explicit reduction

If the real goal is values, keep the pipeline pure when possible.

Performance Heuristics

The sequence abstraction is powerful, but the concrete shape still matters for performance:

  • vectors are great for indexed reads and ordered values
  • sets are great for membership
  • maps are great for keyed data
  • lazy sequences are great when you do not need all values immediately

The mistake is not “using seqs.” The mistake is forgetting that the abstract traversal view does not erase the cost model or semantics of the underlying collection.

Design Review Question

A function takes a vector, runs map and filter over it, then returns the raw sequence result to a caller that immediately indexes into it with nth.

What is the stronger fix?

The stronger fix is usually to make the final result shape explicit with into [] or vec. The transformation pipeline can still use sequence operations, but the contract should match the caller’s indexed-access expectations.

Key Takeaways

  • a sequence is a traversal abstraction, not a concrete collection type
  • sequence functions unify many collection operations, but concrete shape still matters
  • laziness is powerful when the consumer controls realization
  • into, vec, set, and reduction steps make final result shape explicit
  • maps and sets expose seq behavior, but their original semantics still matter
Loading quiz…
Revised on Thursday, April 23, 2026