What Makes a Sequence Lazy?
A lazy sequence in Clojure is a sequence whose elements are computed on demand rather than up front; functions like map, filter, range, and iterate return lazy seqs backed by a chain of unrealized computations wrapped in LazySeq objects. Because nothing is computed until you ask for it, by calling first, seq, or forcing realization another way, you can define sequences that are conceptually infinite, like (range) or (iterate inc 0), without running out of memory.
Cricket analogy: A ball-by-ball radio commentary that only describes a delivery once it's actually bowled, never pre-narrating overs that haven't happened yet, mirrors laziness — computation happens on demand.
Building Infinite Sequences
iterate, repeat, and cycle all produce infinite lazy sequences: (iterate f x) yields x, (f x), (f (f x)), and so on forever; (repeat x) yields x infinitely; (cycle coll) loops coll forever. You must always combine these with a bounding function like take, take-while, or nth to pull a finite, realized piece out — calling something like (count (iterate inc 0)) will hang forever trying to realize an infinite sequence.
Cricket analogy: An automated bowling machine set to fire balls indefinitely at a fixed speed, from which a coach only records the first 6 deliveries for analysis, mirrors (take 6 (repeat ball)).
(take 5 (iterate #(* 2 %) 1)) ;=> (1 2 4 8 16)
(take 4 (cycle [:a :b :c])) ;=> (:a :b :c :a)
(take-while #(< % 100) (iterate inc 1))Calling a realizing function like count, reverse, or into on an infinite lazy sequence without first bounding it with take or take-while will hang the process indefinitely — always bound infinite sequences before forcing realization.
Chunking and Realization Pitfalls
Many core lazy sequences (those built on vectors, like (map f (range 1000000))) are chunked in blocks of 32 for performance, meaning realizing one element can silently force up to 31 more; this matters when your function has side effects, since you may see them fire in unexpected batches of 32. Using dorun or doall explicitly forces realization for its side effects or memory-pinning behavior, and holding onto the head of a lazy sequence (for example, in a def) can cause the whole realized sequence to stay in memory, a classic Clojure memory leak.
Cricket analogy: A TV broadcaster who, when you ask to see just one replay, actually processes a whole batch of 32 camera angles behind the scenes before showing you the one you asked for, mirrors chunked lazy realization.
(def head (map #(do (println "computing" %) %) (range 100)))
(first head) ;; prints "computing 0" through "computing 31" due to chunkingIf you need to force every side effect in a lazy sequence without keeping the results, use (dorun ...); if you also need the realized values retained, use (doall ...).
- Lazy sequences compute elements on demand rather than eagerly, backed by LazySeq objects.
- iterate, repeat, and cycle can produce conceptually infinite sequences.
- Infinite sequences must always be bounded with take, take-while, or similar before being realized.
- Many built-in lazy sequences are chunked in blocks of 32 for performance, which can affect side-effect timing.
- dorun forces realization for side effects without retaining results; doall forces realization and retains results.
- Holding a reference to the head of a large lazy sequence, e.g. via def, can prevent garbage collection and leak memory.
- Laziness lets you express algorithms declaratively without worrying about how much of a sequence will actually be used.
Practice what you learned
1. What does (iterate inc 0) produce?
2. What happens if you call (count (iterate inc 0))?
3. Why might realizing (first head) on a chunked lazy sequence print 32 side-effect lines instead of 1?
4. What is the difference between dorun and doall?
5. Which function would safely extract the first 10 values from an infinite (repeat :x) sequence?
Was this page helpful?
You May Also Like
map, filter, reduce in Clojure
Master the three foundational sequence-processing functions that let you transform, select, and accumulate data declaratively instead of writing explicit loops.
Higher-Order Functions in Clojure
Learn how Clojure treats functions as first-class values, letting you pass them as arguments, return them from other functions, and compose new behavior with comp, partial, and complement.
Threading Macros
Learn how ->, ->>, some->, some->>, and as-> rewrite deeply nested Clojure expressions into readable, linear pipelines without changing runtime behavior.
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