skip to content

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%

answer

  1. Pull-based: one element through the whole chain at a time
  2. Interleaved map/filter, not two full passes
  3. take(2)/first short-circuit and abandon the source
  4. Eager Iterable = full pass per operator
  5. Side effects run in interleaved order

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.

solid answer

~40 s

Sequences are pull-based and process **element-by-element, depth-first** through the chain: the terminal op asks for the next element, which pulls one item through map, then filter, then take, before requesting the next. So for `(1..5).asSequence().map{}.filter{}.take(2).toList()`, you see map(1), filter, map(2), filter, ... interleaved — not map(1..5) then filter(1..5). The `take(2)` is a stateful intermediate that signals completion after the 2nd element survives filtering, so iteration **short-circuits**: remaining source elements are never mapped or filtered. This is the opposite of `Iterable` (eager) operators, where `map` builds a full list, then `filter` builds another full list, then `take` slices it — every element is processed. The interleaving plus short-circuit is the whole performance argument for sequences.

code

kotlin · 6 lines
kotlin
(1..5).asSequence()
    .map { println("map $it"); it * 10 }
    .filter { println("filter $it"); it > 20 }
    .take(2)
    .toList()
// prints map/filter interleaved; map 5 never runs; result [30, 40]

go deeper

for a junior

Knows nothing runs until the terminal and can state map/filter are lazy on a sequence.

for a middle

Traces the interleaved, element-by-element order and explains take short-circuiting the source.

for a senior

Articulates the pull-based Iterator model and contrasts wasted work vs eager Iterable passes.

for a principal

Uses the execution model to reason about side-effect ordering, observability, and when laziness is a footgun vs a win.

## How a Sequence pipeline actually executes The key idea: a `Sequence` is **pull-based**. The terminal operation owns the loop and repeatedly asks the chain for the *next* element via an `Iterator`. Each request flows up the chain, and a single source element is pushed all the way down before the next is requested. ### Trace example ```kotlin val result = (1..5).asSequence() .map { println("map $it"); it * 10 } .filter { println("filter $it"); it > 20 } .take(2) .toList() ``` Execution order printed: ``` map 1 -> 10 filter 10 -> dropped (10 > 20 is false) map 2 -> 20 filter 20 -> dropped map 3 -> 30 filter 30 -> kept (1st of take) map 4 -> 40 filter 40 -> kept (2nd of take) -> take is satisfied // map 5 / filter 50 NEVER run ``` Result: `[30, 40]`. Element 5 is **never mapped or filtered** because `take(2)` reported it had enough. ### Why this order? - **Depth-first / interleaved**: each terminal `next()` pulls exactly one item through the full chain. So `map` and `filter` alternate per element rather than running in two full passes. - **Short-circuit**: `take(n)` is a *stateful* intermediate; once it has emitted `n` items it stops calling upstream, so the source iterator is abandoned early. `first`, `find`, `any`, `firstOrNull` short-circuit similarly. ### Contrast with eager Iterable ```kotlin (1..5).map { it * 10 } // full list [10,20,30,40,50] built .filter { it > 20 } // full list [30,40,50] built .take(2) // [30,40] ``` Here `map` runs for **all five** elements, then `filter` for all five — no interleaving, no early stop based on the terminal slice. ### Practical implications - For large/expensive pipelines with an early-exit terminal, sequences avoid wasted work. - Side-effecting lambdas (logging, mutation) execute in the interleaved order, which can surprise people relying on "all maps, then all filters."

  • Why is element 5 never processed in the example?
    take(2) becomes satisfied after the 2nd surviving element (40), so it stops pulling from upstream; the source iterator is never asked for element 5.
  • How would the order change if these were eager Iterable operators?
    map would run for all 5 elements building a full list, then filter for all 5 — no interleaving and take couldn't prevent earlier work.

saying these in an interview costs you the question

  • Claiming all map calls happen before any filter call on a sequence
  • Saying take(2) still processes every source element
  • Not recognizing the depth-first per-element flow
  • Believing sequences and Iterables execute lambdas in the same order
  • Thinking the terminal op is irrelevant to which elements get processed

context