The Sequence Abstraction
Clojure defines a uniform abstraction called a sequence, or seq, that any collection can be viewed through. A seq supports three core operations: first returns the first element, rest returns a seq of the remaining elements, and cons prepends an element to produce a new seq. Because vectors, lists, maps, sets, and even strings can all be converted to a seq via the seq function, the same handful of sequence functions work identically regardless of the underlying concrete type — (first {:a 1 :b 2}) returns a map entry like [:a 1], just as (first [1 2 3]) returns 1.
Cricket analogy: The seq abstraction is like the concept of 'the next ball' applying identically whether you're watching a Test match at Lord's, an ODI, or a T20 — first and rest let you process deliveries the same way regardless of the format underneath.
Laziness and Infinite Sequences
Many sequence-producing functions in Clojure are lazy: they don't compute their elements until those elements are actually needed. range without an upper bound, (range), produces an infinite lazy sequence of increasing integers, and functions like map and filter applied to it also stay lazy, only realizing values as they're consumed. This lets you write (take 5 (map inc (range))), which safely returns (1 2 3 4 5) even though (range) never terminates, because take only pulls the first five values it needs and no more. The lazy-seq macro is the primitive used to build custom lazy sequences by wrapping a computation so it runs only on first access.
Cricket analogy: Laziness is like a live ball-by-ball commentary feed at the Boxing Day Test that only generates commentary for the next ball when the broadcaster actually calls for it, rather than pre-scripting commentary for an entire never-ending match in advance.
Core Sequence-Processing Functions
A small set of functions built on the seq abstraction cover most everyday data processing: map transforms each element, filter keeps elements matching a predicate, reduce folds a sequence into a single accumulated value, take/drop slice a prefix or suffix, and sort-by orders elements by a derived key. These compose naturally: (->> [5 2 8 1 9] (filter odd?) (map inc) (reduce +)) filters to (5 1 9), increments each to (6 2 10), and sums them to 18, all expressed as a readable left-to-right pipeline using the ->> threading macro.
Cricket analogy: Chaining filter, map, and reduce is like a national selector first filtering players by 'batting average above 40,' then mapping each to their strike rate, then reducing to a single squad total strike rate — three clear steps in a readable pipeline.
Sequences vs Concrete Collections
It's important to distinguish between a sequence — the abstract, lazy, potentially infinite view — and the concrete collections like vectors and maps that seq functions operate over. Calling a seq function such as map or filter on a vector doesn't return another vector; it returns a lazy sequence. If you need a concrete collection type back, you must convert explicitly, for example with (vec (map inc [1 2 3])) to get a vector, or (into #{} (filter even? [1 2 3 4])) to collect results into a set. This distinction matters for performance and for code that depends on a specific concrete type downstream.
Cricket analogy: The difference between a seq and a concrete collection is like the difference between live ball-by-ball commentary text during a T20 (a flowing stream) and the final printed scorecard (a fixed, structured document) — you often need to explicitly compile the commentary stream into the scorecard format.
;; The seq abstraction works uniformly across types
(first [1 2 3]) ;=> 1
(first '(1 2 3)) ;=> 1
(first {:a 1 :b 2}) ;=> [:a 1]
(rest [1 2 3]) ;=> (2 3)
;; Lazy, infinite sequences
(take 5 (map inc (range))) ;=> (1 2 3 4 5)
(take 3 (filter even? (range))) ;=> (0 2 4)
;; Composing a pipeline with the ->> threading macro
(->> [5 2 8 1 9]
(filter odd?)
(map inc)
(reduce +))
;=> 18
;; Converting a lazy seq back to a concrete collection
(vec (map inc [1 2 3])) ;=> [2 3 4]
(into #{} (filter even? [1 2 3 4])) ;=> #{2 4}
The threading macro ->> inserts each preceding expression's result as the last argument of the next form, letting you write data-processing pipelines top-to-bottom in the order operations actually happen — filter, then map, then reduce — instead of nesting them inside-out.
Building an infinite lazy sequence like (range) and calling a non-lazy, fully-consuming function on it directly — such as (count (range)) or (reduce + (range)) — will never terminate. Always bound an infinite sequence with take or a predicate-based function like take-while before realizing it fully.
- The seq abstraction provides three core operations — first, rest, and cons — that work uniformly across lists, vectors, maps, sets, and strings.
- Calling seq on a collection produces a logical view; the same processing functions work regardless of the underlying concrete type.
- Many sequence functions, including map, filter, and range (with no arguments), are lazy and only compute values as they're consumed.
- take and take-while safely bound infinite lazy sequences by pulling only the elements actually needed.
- The ->> threading macro lets you write filter/map/reduce pipelines in a readable, left-to-right order.
- Sequence functions like map and filter return lazy sequences, not the original concrete collection type — use vec or into to convert back.
- Fully realizing an infinite lazy sequence with functions like count or non-bounded reduce will hang the program.
Practice what you learned
1. What does (first {:a 1 :b 2}) return?
2. What makes (take 5 (map inc (range))) safe to use?
3. What does calling (map inc [1 2 3]) return?
4. In (->> [5 2 8 1 9] (filter odd?) (map inc) (reduce +)), what is the result?
5. What is the risk of calling (count (range))?
Was this page helpful?
You May Also Like
Immutable Data Structures
Explore Clojure's core persistent data structures — vectors, lists, maps, and sets — and understand how immutability and structural sharing make them both safe and efficient.
Functions in Clojure
Learn how to define, call, and compose functions in Clojure, from simple defn forms to anonymous functions, multi-arity definitions, and higher-order functions.
Recursion and loop/recur
Learn how Clojure handles recursion without mutable loop variables, and how the recur special form enables safe, stack-efficient tail recursion via loop and function self-calls.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics