Sequences (Lazy Collections)
A sequence is a lazily-evaluated collection that processes elements one at a time through an entire operation chain, only doing work when a terminal operation finally pulls the results.
Learn Sequences (Lazy Collections) in our free Kotlin course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick…
Part of the free Kotlin course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
You'll learn asSequence() , the difference between lazy and eager evaluation, generateSequence , terminal operations, and exactly when sequences beat plain lists.
What You'll Learn in This Lesson
1️⃣ Eager Lists: Each Step Finishes First
Ordinary collection operations are eager . When you chain .map { } .filter { } on a list, map runs over every element and builds a whole new list, then filter runs over that. The print statements below reveal the order: all the maps, then all the filters.
Every map runs before any filter , and an intermediate list of all four doubled values exists in memory in between.
2️⃣ Sequences: One Element All the Way Through
Call .asSequence() and the same chain becomes lazy . Now each element travels through the entire pipeline before the next one starts, and no work happens at all until a terminal operation such as toList() pulls the values. Watch how map and filter now interleave.
No intermediate list is built, and because elements flow one at a time, a sequence can stop early — first { } would halt as soon as it found a match instead of transforming everything first.
3️⃣ generateSequence: Infinite, but Lazy
Because sequences only compute what's consumed, they can be infinite . generateSequence(seed) { next } starts from a seed and repeatedly applies a function. Pair it with take(n) and a terminal operation to get exactly the values you need and not one more.
Without laziness, an infinite source like this could never finish. With it, take(5) computes precisely five values.
Your turn. Fill in the ___ , then run and compare.
Mini-Challenge: A Lazy Pipeline
Combine generateSequence with a lazy filter/map chain and an early take .
Common Errors (and the fix)
- ❌ Nothing prints / nothing happens — You built a sequence but never added a terminal operation. ✅ End the chain with toList() , sum() , first() , etc.
- ❌ Program hangs forever — You called a terminal like toList() on an infinite generateSequence without take(n) . ✅ Limit it with take or takeWhile first.
- ❌ Using a sequence for a tiny list — The lazy machinery adds overhead. ✅ For short chains over small collections, plain list operations are simpler and just as fast.
- ❌ Iterating a sequence twice — Some sequences are single-use and re-running them re-executes side effects. ✅ Materialize with toList() if you need the result more than once.
Pro Tips
- 💡 Long chains + big data = sequence: the more operations and the larger the input, the more laziness pays off.
- 💡 Early exit is free: asSequence().first { ... } stops at the first match instead of processing everything.
- 💡 Same result, different timing: a list chain and a sequence chain produce identical answers — sequences just avoid intermediate collections.
📋 Quick Reference — Sequences
Concept
Kotlin Syntax
From a collection
list.asSequence()
Infinite sequence
generateSequence(1) { it + 1 }
Intermediate op
.map { } / .filter { }
Limit values
.take(5)
Terminal op
.toList() / .sum() / .first()
Build directly
sequenceOf(1, 2, 3)
Frequently Asked Questions
🎉 Lesson Complete!
- ✅ Lists are eager; sequences are lazy
- ✅ In a sequence each element flows through the whole chain at once
- ✅ Work only happens at a terminal operation
- ✅ generateSequence models infinite streams safely with take
- ✅ Sequences win for long chains, large data, and early exits
- ✅ Next lesson: Interfaces & Abstract Classes
Practice quiz
How is a List chain evaluated?
- Lazily, one element at a time
- Only when printed
- Eagerly: each operation completes and builds a new list before the next
- In random order
Answer: Eagerly: each operation completes and builds a new list before the next. List operations are eager; each step builds a full intermediate list before the next runs.
How is a Sequence evaluated?
- Lazily: nothing runs until a terminal operation pulls values
- Eagerly, like a list
- All at once at declaration
- Never
Answer: Lazily: nothing runs until a terminal operation pulls values. A sequence is lazy; work happens only when a terminal operation requests results.
In a sequence chain, how does each element move through the operations?
- All elements finish map, then all finish filter
- Elements are processed in parallel
- Only the first element is processed
- One element flows through the whole chain before the next starts
Answer: One element flows through the whole chain before the next starts. In a sequence each element travels through the entire pipeline one at a time.
Which converts a list into a lazy sequence?
- toList()
- asSequence()
- toSequence()
- lazy()
Answer: asSequence(). asSequence() turns a collection into a lazily-evaluated sequence.
What happens if a sequence chain has no terminal operation?
- No work happens at all
- It runs immediately
- It throws an exception
- It loops forever
Answer: No work happens at all. Without a terminal operation, intermediate operations never execute.
Which of these is a terminal operation?
- map
- filter
- toList
- take
Answer: toList. toList (along with sum, first, count, forEach) is terminal; map/filter/take are intermediate.
What does generateSequence(1) { it * 2 } produce?
- A finite list of two values
- A lazy, conceptually infinite sequence: 1, 2, 4, 8, ...
- A compile error
- Only the number 1
Answer: A lazy, conceptually infinite sequence: 1, 2, 4, 8, .... generateSequence builds a lazy, potentially infinite sequence from a seed and a next function.
Why must you usually pair an infinite generateSequence with take(n)?
- take makes it faster
- take is required syntactically
- To make it eager
- Otherwise a terminal like toList() never finishes
Answer: Otherwise a terminal like toList() never finishes. Limiting with take(n) prevents a terminal operation from running forever on an infinite source.
When do sequences typically outperform plain list operations?
- For very short chains on tiny lists
- For long chains, large data, or an early exit
- Always, in every case
- Only when printing
Answer: For long chains, large data, or an early exit. Laziness pays off with long chains, big data, and early exits that avoid extra work.
What is the relationship between a list chain and the equivalent sequence chain's final result?
- The sequence gives a different answer
- The list is always wrong
- They produce identical results; only the timing and memory differ
- The sequence drops elements
Answer: They produce identical results; only the timing and memory differ. Both produce the same final result; sequences just avoid intermediate collections.
Continue this course
- Previous: Inline Functions & reified
- Next: Interfaces & Abstract Classes