skip to content

Lazy Evaluation Model

A Sequence pulls one element through every operator before touching the next, while collection operators build a full intermediate list at each stage. asSequence() is how you opt into that model, and knowing why it changes evaluation order is the point.

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

questions

5

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%

answer

  1. List = eager, new list each step (stage-at-a-time)
  2. Sequence = lazy, element-at-a-time through whole chain
  3. asSequence() opts in; toList() opts out
  4. Nothing runs until a terminal op pulls
  5. first()/take short-circuit on a sequence

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.

solid answer

~40 s

On a List, every operator (map, filter, take) is eager: it runs over the whole collection and returns a brand-new List, so a 3-step chain allocates 3 intermediate lists. asSequence() converts the Iterable into a Sequence<T>, where intermediate operators are lazy and just wrap the previous step. Nothing runs until a terminal operation (toList, first, sum, forEach, count) pulls elements. Then each element is pushed through the entire pipeline before the next element starts — element-at-a-time, not stage-at-a-time. This avoids intermediate list allocations and lets short-circuiting terminals like first or take stop early without processing the rest. You opt back to a List with toList()/toSet().

code

kotlin · 15 lines
kotlin
val nums = listOf(1, 2, 3, 4)

// Eager: prints all filter results, then all map results
nums.filter { print("f$it "); it % 2 == 0 }
    .map { print("m$it "); it * 10 }
// f1 f2 f3 f4 m2 m4

println()

// Lazy: interleaved, element-at-a-time
nums.asSequence()
    .filter { print("f$it "); it % 2 == 0 }
    .map { print("m$it "); it * 10 }
    .toList()
// f1 f2 m2 f3 f4 m4

go deeper

for a junior

Knows asSequence() makes things lazy and that a terminal op is needed to run anything.

for a middle

Can explain stage-at-a-time (List) vs element-at-a-time (Sequence) and intermediate-vs-terminal distinction.

for a senior

Articulates intermediate list allocations avoided and short-circuiting via first/take, and when laziness is actually worth it.

for a principal

Frames it as a streaming/pull model and reasons about its cost/benefit across realistic data sizes and chain shapes.

## The two evaluation models Kotlin gives you two ways to run a transformation pipeline: - **Eager (collections / Iterable):** Calling `map`, `filter`, `take` etc. directly on a `List` runs **immediately** and returns a **new `List`** at every step. This is *stage-at-a-time*: the whole collection goes through stage 1, producing a full intermediate list; that whole list goes through stage 2, producing another full list; and so on. - **Lazy (`Sequence<T>`):** `asSequence()` turns an `Iterable` into a `Sequence`. Intermediate operators (`map`, `filter`, …) **don't run** — they just return a wrapper sequence describing the work. Execution only happens when a **terminal operation** (`toList`, `first`, `sum`, `count`, `forEach`, `find`) asks for results. Then it runs *element-at-a-time*: one element is pulled and pushed through **the entire chain** before the next element is touched. ## Why it matters ```kotlin val list = (1..1_000_000).toList() // EAGER: allocates a full filtered list, then a full mapped list val a = list.filter { it % 2 == 0 }.map { it * 2 }.first() // LAZY: pulls 1, fails filter; pulls 2, passes, maps to 4, first() stops. Two elements touched. val b = list.asSequence().filter { it % 2 == 0 }.map { it * 2 }.first() ``` The eager version builds two million-ish-element intermediate lists just to grab the first result. The sequence version short-circuits after the second element because `first()` only needs one value and the laziness lets earlier stages stop pulling. ## Key APIs / keywords - `asSequence()` — opt in to laziness from any `Iterable`/`Array`/`Map`. - `Sequence<T>` interface with a single `iterator()`. - **Intermediate** operators return `Sequence<T>` (lazy): `map`, `filter`, `take`, `drop`, `distinct`. - **Terminal** operators trigger evaluation and return a concrete value: `toList`, `toSet`, `first`, `sum`, `count`, `forEach`. - `toList()` / `toSet()` — go back from a lazy `Sequence` to an eager collection. ## Mental model A `Sequence` is a recipe, not a meal. `asSequence()` writes the recipe lazily; the terminal operation cooks it, one ingredient pushed all the way through before the next.

  • If you call asSequence().map { ... } and never add a terminal operation, does the map lambda run?
    No. Intermediate operators are lazy; with no terminal op nothing is pulled, so the lambda never executes.
  • How do you turn the sequence back into a List?
    Call a terminal collector like toList() or toSet() (or toMutableList()).

A List pipeline is an assembly line that finishes each station for the whole batch; a Sequence carries one part through every station before the next part starts.

saying these in an interview costs you the question

  • Saying map/filter on a Sequence run immediately like on a List
  • Claiming sequences always allocate intermediate lists
  • Forgetting that a terminal operation is required to trigger work
  • Thinking asSequence() copies the data into a new collection eagerly

context

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

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

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

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