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.
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.
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:
seqseq 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 nextThese 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.
conscons 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.
Many sequence-producing functions are lazy:
mapfilterremovetakedroprepeatiterate1(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.
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:
into, vec, set, or a reducing step choose the final shapeWhen 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 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.
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:
doseqrun!If the real goal is values, keep the pipeline pure when possible.
The sequence abstraction is powerful, but the concrete shape still matters for performance:
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.
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.
into, vec, set, and reduction steps make final result shape explicit