skip to content

Sequences & Lazy Pipelines

Sequence is Kotlin's lazy pipeline type: elements flow through the whole chain one at a time instead of materializing a list per step. When to switch to it is one of the most common practical Kotlin questions.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

explore

questions

25

What does generateSequence(seed) { next } do, and how do you stop it from producing values forever?

level: juniorimportance: must knowfreq 55%

answer

  1. Seed is the first element
  2. next(prev) produces each element
  3. Returning null stops it (null not emitted)
  4. Infinite unless you stop or take()
  5. Lazy: terminal op drives evaluation

basics

~10 s

It makes a lazy stream that starts at the seed and repeatedly applies your function to get the next value. It stops when your function returns null. Until then, it never ends.

solid answer

~40 s

generateSequence(seed) { next } returns a Sequence<T> built lazily: the first element is the seed, and each subsequent element is produced by calling the lambda with the previous element. The sequence terminates the first time the lambda returns null (that null is not emitted). Because it is a Sequence, nothing runs until a terminal operation (toList, first, sum, etc.) pulls elements, and elements are computed one at a time on demand. To bound an otherwise infinite sequence you either return null from the generator at the stop condition, or use take(n)/takeWhile { }/first { } on the consumer side. Example: generateSequence(1) { it * 2 }.take(5).toList() yields [1, 2, 4, 8, 16].

code

kotlin · 5 lines
kotlin
val powers = generateSequence(1) { (it * 2).takeIf { v -> v <= 100 } }
println(powers.toList()) // [1, 2, 4, 8, 16, 32, 64]

val naturals = generateSequence(1) { it + 1 }
println(naturals.take(5).toList()) // [1, 2, 3, 4, 5]

go deeper

for a junior

Knows seed is first element, next produces the rest, and null stops it; can bound with take.

for a middle

Explains laziness and that terminal ops drive evaluation; uses takeWhile/takeIf to stop cleanly.

for a senior

Discusses infinite-sequence hazards, short-circuiting terminals, and operator fusion per element.

for a principal

Frames when a lazy generator beats eager collection building and reasons about purity/cost of nextFunction in hot paths.

## What it is `generateSequence` is a top-level function in the Kotlin standard library that builds a **`Sequence<T>`** — a *lazy*, pull-based collection. "Lazy" means no value is computed until something downstream actually asks for it (a *terminal* operation like `toList()`, `first()`, `sum()`, `forEach`). ## The seeded overload ```kotlin fun <T : Any> generateSequence(seed: T?, nextFunction: (T) -> T?): Sequence<T> ``` - The **first element** is `seed`. - Each following element is `nextFunction(previousElement)`. - The sequence **stops** the moment `nextFunction` returns `null`. That `null` is the terminator and is **not** included. - Note the types: `seed` is `T?` and `nextFunction` returns `T?`. If `seed` is `null`, the sequence is empty. ```kotlin // powers of two until they exceed 100 generateSequence(1) { prev -> (prev * 2).takeIf { it <= 100 } } .toList() // [1, 2, 4, 8, 16, 32, 64] ``` ## Infinite by default If `nextFunction` never returns `null`, the sequence is **infinite**: ```kotlin val naturals = generateSequence(1) { it + 1 } // 1, 2, 3, ... ``` Calling a terminal that consumes everything (e.g. `toList()`, `count()`) on an infinite sequence loops forever / overflows. You must bound it on the consumer side with **`take(n)`**, **`takeWhile { }`**, **`first()`/`first { }`**, or **`find { }`** — these are short-circuiting and stop pulling once satisfied. ```kotlin naturals.take(5).toList() // [1, 2, 3, 4, 5] naturals.first { it % 7 == 0 } // 7 ``` ## Laziness in practice Intermediate operators (`map`, `filter`, `take`) are also lazy and fuse element-by-element. Nothing executes at the call site: ```kotlin val s = generateSequence(1) { it + 1 } .map { println("map $it"); it * it } // nothing printed yet val r = s.take(3).toList() // NOW it runs: map 1, map 2, map 3 ``` ## Common idioms - Walk a parent chain (e.g. exceptions, tree nodes): `generateSequence(node) { it.parent }`. - Read lines until EOF: `generateSequence { reader.readLine() }` (seedless overload — see below). - Numeric series with a stop condition via `takeIf`. Keep `nextFunction` **pure and cheap**; it is invoked once per produced element during iteration.

  • Is the null that stops the sequence included as an element?
    No. The null acts purely as a terminator; the last emitted element is the last non-null value the function returned.
  • What happens if seed is null?
    The sequence is empty — iteration ends immediately because the terminator condition is met before any element is produced.

Like a snowball rolling downhill: you give it a starting size (seed), and each push (next) makes the next size — it keeps rolling until you decide to stop it (null or take).

saying these in an interview costs you the question

  • Saying it returns a List eagerly
  • Thinking the stopping null becomes the last element
  • Calling toList() on an infinite sequence and not seeing the hang problem
  • Believing map/filter run immediately at the call site
  • Confusing the stop condition (null) with throwing an exception

context

open as a page

On a Kotlin Sequence, what is the difference between an intermediate operation and a terminal operation? Give examples of each.

level: juniorimportance: must knowfreq 70%

basics

~10 s

Intermediate operations like map and filter just describe work and return a new sequence without doing anything yet. Terminal operations like toList or sum actually run the pipeline and give you a final result.

open as a page

What does the sequence{} builder do in Kotlin, and how do you emit elements from inside it?

level: juniorimportance: must knowfreq 60%

basics

~10 s

sequence{} creates a lazy sequence. Inside the block you call yield(x) to hand out one value at a time, only producing them as the consumer asks for them.

open as a page

What does asSequence() do to a collection chain, and how does processing differ from calling map/filter directly on a List?

level: juniorimportance: must knowfreq 70%

basics

~10 s

asSequence() makes the chain lazy. Instead of building a new list after each map/filter, each element flows through every step one at a time, only when a result is finally needed.

open as a page

What is the difference between a List and a Sequence in Kotlin, and when would you choose a Sequence for performance?

level: juniorimportance: must knowfreq 80%

basics

~20 s

A List runs each step fully and makes a new list every step. A Sequence is lazy: it pulls one element all the way through the chain. Use Sequences for long chains over large data.

open as a page

Given a sequence with map then filter then a take(2) and toList terminal, trace the exact order in which the lambdas execute and explain why.

level: middleimportance: must knowfreq 60%

basics

~10 s

Each element is pushed through map then filter one at a time, not all maps first. The take(2) stops the whole thing as soon as two elements pass, so later elements are never touched.

open as a page

Predict the print order for a List chain versus an asSequence() chain with side-effecting filter and map, and explain why they differ.

level: middleimportance: must knowfreq 60%

basics

~20 s

On a List each step finishes for all elements first, so you see all filters then all maps. On a Sequence each element travels through filter then map before the next, so the output is interleaved.

open as a page

How does short-circuiting work with Sequences, and why does `asSequence().filter{}.first{}` avoid scanning the whole collection?

level: middleimportance: must knowfreq 65%

basics

~10 s

A Sequence pulls elements one by one. Terminal operations like first, find, take, and any stop the moment they have their answer, so the rest of the data is never touched.

open as a page

How would you generate a Fibonacci sequence with generateSequence, and what does this reveal about carrying state in the seeded overload?

level: middleimportance: should knowfreq 35%

basics

~20 s

Because each Fibonacci number needs the two before it, you make the element a pair (a, b). The seed is the first pair, and next turns (a, b) into (b, a+b). Then map each pair to its first number.

open as a page

What is the seedless overload generateSequence { ... }, and when would you use it instead of the seeded form?

level: middleimportance: should knowfreq 40%

basics

~20 s

It's a version where you give just one function that produces each next value (using outside state), and it stops when that function returns null. Great for reading until there's nothing left, like lines from a file.

open as a page

Can you call two terminal operations on the same Sequence instance? What happens, and how does this differ from calling a terminal op without one at all?

level: middleimportance: should knowfreq 45%

basics

~20 s

Some sequences can only be iterated once, so running a second terminal op on them throws. A sequence built from a collection can be re-run. And without any terminal op, the chain never executes at all.

open as a page

A Sequence built with sequence{} is iterated twice. What happens, and what does that imply about side effects in the builder block?

level: middleimportance: should knowfreq 28%

basics

~10 s

Each iteration re-runs the builder block from the top. So the values are recomputed, and any side effects (like printing) happen again every time you consume the sequence.

open as a page

What is the difference between yield and yieldAll inside a sequence{} block, and which overloads does yieldAll accept?

level: middleimportance: should knowfreq 45%

basics

~10 s

yield emits one element. yieldAll emits a whole group at once. yieldAll works with a list/iterable, another sequence, or an iterator — and it stays lazy.

open as a page

Explain, in terms of the lazy evaluation model, why a List chain allocates intermediate collections and a Sequence chain does not. How many intermediate lists does each create for filter→map→take?

level: middleimportance: should knowfreq 50%

basics

~20 s

Each List operator returns a brand-new list, so filter→map→take makes three intermediate lists. A Sequence just wraps the previous step lazily and pushes elements one by one, so it makes zero intermediate lists until the terminal collector.

open as a page

Why can converting a small list to a Sequence actually be slower, and how would you decide whether a Sequence is worth it?

level: middleimportance: should knowfreq 55%

basics

~10 s

Sequences add overhead per element: iterator objects and lambda calls for every step. On small lists or a single operation, that overhead beats the savings, so plain List operators are faster.

open as a page

When is generateSequence the right tool versus building a list eagerly or writing a manual loop, and what performance/correctness traits should drive that choice?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Use generateSequence when the data is unbounded or you only need part of it, so you don't compute or store more than necessary. If you need the whole finite set and will reuse it many times, an eager list can be simpler and faster.

open as a page

Among intermediate sequence operations, what is the difference between stateless and stateful ones? Why does it matter for laziness and infinite sequences?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Stateless intermediate ops like map and filter handle one element at a time. Stateful ones like sorted and distinct need to look at many or all elements, so they can break laziness and will hang on an infinite sequence.

open as a page

Why can't you call suspend functions like delay() inside a sequence{} block, and what mechanism enforces that?

level: seniorimportance: should knowfreq 30%

basics

~10 s

The sequence builder is built on a restricted kind of coroutine. The compiler only lets you call yield/yieldAll inside it, so you can't await real async work like delay().

open as a page

Sequences are lazily/deferred-evaluated. What surprising behaviors or pitfalls does this deferral cause around side effects, single-iteration, and exception timing?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Because work is deferred until a terminal op, side effects in map/filter don't run when you write them, exceptions surface later, and some sequences can only be iterated once or re-run their work on each terminal call.

open as a page

Analyze allocation and pass count for a multi-step pipeline run eagerly vs as a Sequence over large data. What exactly differs?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Eager makes a new list at every step and walks the whole data once per step. A Sequence makes no intermediate lists and walks each element once through all steps, stopping early if a terminal allows.

open as a page

What is the difference between onEach and forEach on a Sequence, and when would each pull from a real-world standpoint?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

onEach is intermediate: it runs a side effect on each element but passes the element along, so the chain keeps going lazily. forEach is terminal: it runs a side effect and ends the chain, returning nothing.

open as a page

Design a generateSequence-based pipeline to consume a cursor-paginated API until exhaustion, and explain the laziness and termination guarantees you rely on.

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

Make each element a page. Start by fetching the first page (seed), then next fetches the page after the current one using its cursor, returning null when there's no next page. Flatten the pages into items and take what you need.

open as a page

How does the iterator{} builder relate to sequence{}, and when would you reach for sequence{} over a plain List or generateSequence?

level: seniorimportance: nice to knowfreq 18%

basics

~10 s

iterator{} works just like sequence{} but returns an Iterator instead of a Sequence. Use sequence{} when generation logic is branchy or recursive, or when the stream is infinite/expensive and you want laziness.

open as a page

Given the lazy element-at-a-time model, when does converting a List chain to asSequence() actually help, and when can it be neutral or slower? Justify from the model, not benchmarks.

level: seniorimportance: nice to knowfreq 35%

basics

~20 s

Laziness helps with large inputs, long chains, or when you need only part of the result (first/take): it skips intermediate lists and stops early. For tiny inputs or a single operator it adds overhead and can be neutral or slower.

open as a page

You own a shared data-processing library. How do you decide your public API returns List vs Sequence, and what pitfalls do you warn callers about?

level: principalimportance: nice to knowfreq 25%

basics

~20 s

Return a List when results are small, finite, and reused; return a Sequence when data may be large or infinite and callers can stream and stop early. Warn that Sequences are lazy and may be single-use.

open as a page