skip to content

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%

answer

  1. List: all F's then all M's (stage-at-a-time)
  2. Sequence: F1 M1 F2 F3 M3 (element-at-a-time)
  3. Sequence ops are pull-based iterators
  4. Rejected elements skip downstream stages
  5. toList() drives the pulling

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.

solid answer

~40 s

The difference is *stage-at-a-time* vs *element-at-a-time*. With an eager List, `filter` runs over every element and produces a complete intermediate list, then `map` runs over that list — so the log shows every `filter` call grouped, then every `map` call grouped. With `asSequence()`, evaluation is driven by the terminal `toList()`: it pulls one element through the **whole** chain (filter, then map if it passed) before pulling the next. So logs interleave per element. The element-at-a-time model is exactly why sequences avoid intermediate collections and can short-circuit: each element's full journey is computed on demand, and a stage only requests the next element when its consumer asks for one.

code

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

// Eager
data.filter { print("F$it "); it != 2 }
    .map { print("M$it "); it }
// -> F1 F2 F3 M1 M3

println()

// Lazy
data.asSequence()
    .filter { print("F$it "); it != 2 }
    .map { print("M$it "); it }
    .toList()
// -> F1 M1 F2 F3 M3

go deeper

for a junior

Can identify that List groups side effects and Sequence interleaves them, even if shaky on why.

for a middle

Predicts both exact orders and explains stage- vs element-at-a-time correctly.

for a senior

Explains the pull-based iterator implementation and ties it to allocation savings and short-circuiting.

for a principal

Discusses how side-effect ordering assumptions become latent bugs when code is refactored between eager and lazy, and how to design pipelines to be effect-free.

## Setup ```kotlin val data = listOf(1, 2, 3) fun process(seq: Boolean) { val base = if (seq) data.asSequence() else data base.filter { print("F$it "); it != 2 } .map { print("M$it "); it } .let { if (seq) (it as Sequence<Int>).toList() else it } } ``` ## Eager List output ``` F1 F2 F3 M1 M3 ``` `filter` is **eager**: it iterates the entire list, calling the predicate for 1, 2, 3, and builds the intermediate list `[1, 3]`. Only then does `map` iterate that list, calling its lambda for 1 and 3. All `F`s precede all `M`s — **stage-at-a-time**. ## Lazy Sequence output ``` F1 M1 F2 F3 M3 ``` The terminal `toList()` pulls element 1: `filter` passes it (`F1`), so `map` runs (`M1`). Then it pulls 2: `filter` rejects it (`F2`), so `map` never runs for 2. Then 3: `filter` passes (`F3`), `map` runs (`M3`). Each element completes its full path before the next — **element-at-a-time**. ## Why it happens — the pull model A `Sequence`'s operators are implemented as iterators that **pull** from upstream. The terminal asks the top operator for the next value; that operator asks the one below it; and so on down to the source. Each `next()` call walks one element through the chain. Eager collection operators instead **materialize** a whole new `List` per call. ## Practical consequences - **No intermediate lists** with sequences — less allocation/GC. - **Short-circuiting**: `first`, `take(n)`, `find` stop pulling early, so unreached elements never enter the pipeline. - **Side effects order** differs — never rely on eager grouping of side effects when you might switch to a sequence.

  • In the sequence version, how many times does the map lambda run, and why?
    Twice (for 1 and 3). Element 2 is rejected by filter, so it never reaches map.
  • Would adding .take(1) to the sequence change how many filter calls happen?
    Yes. take(1) stops after the first passing element, so filter runs only for element 1 (F1) and pulling stops.

saying these in an interview costs you the question

  • Predicting interleaved output for the eager List version
  • Predicting grouped output (all F then all M) for the sequence version
  • Saying map runs for the filtered-out element 2
  • Not connecting interleaving to the pull-based iterator model

context