skip to content

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%

answer

  1. Intermediate returns Sequence; terminal returns a result
  2. Nothing runs until a terminal op
  3. map/filter/take = intermediate
  4. toList/sum/first/forEach = terminal
  5. Forgot terminal = nothing executes

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.

solid answer

~40 s

On a Sequence, intermediate operations (map, filter, take, drop, distinct, mapIndexed) are lazy: each returns a new Sequence wrapping the previous one and does no work until a terminal op runs. Terminal operations (toList, toSet, sum, count, first, last, find, forEach, reduce, fold, any, all, joinToString) start the iteration, pull elements through the whole chain, and produce a concrete result (a collection, a scalar, or a side effect). Nothing executes until a terminal op is invoked, which is why a sequence with only intermediate ops prints nothing. This contrasts with Iterable/List operators, which are eager: each step materializes a full intermediate list immediately. The mental model: intermediate = build the recipe, terminal = cook the meal.

code

kotlin · 5 lines
kotlin
val seq = sequenceOf(1, 2, 3)
    .map { it * 2 }      // intermediate, lazy
    .filter { it > 2 }   // intermediate, lazy
// seq is a Sequence<Int>; no work done yet
val list = seq.toList() // terminal: runs the chain -> [4, 6]

go deeper

for a junior

Can name map/filter as intermediate and toList/sum as terminal and state that nothing runs without a terminal.

for a middle

Explains that each intermediate returns a new Sequence and work is deferred until the terminal pulls elements.

for a senior

Connects laziness to no intermediate allocations and short-circuiting, and contrasts with eager Iterable operators.

for a principal

Frames the intermediate/terminal split as the core of the pull-based execution model and weighs it when designing data-processing APIs.

## The two kinds of Sequence operations A `Sequence<T>` in Kotlin models a **lazy** stream of elements. Operations on it fall into exactly two categories. ### Intermediate operations An **intermediate** operation returns another `Sequence<R>` and performs **no computation** when called. It only wraps the upstream sequence, recording "when someone eventually iterates me, apply this transform." Examples: - `map`, `mapIndexed`, `mapNotNull` - `filter`, `filterNot`, `filterIsInstance` - `take`, `takeWhile`, `drop`, `dropWhile` - `distinct`, `sorted`, `flatMap`, `onEach`, `withIndex` Because they return a `Sequence`, intermediate ops are **chainable** and **stateless to call** — the work is deferred. ### Terminal operations A **terminal** operation consumes the sequence: it starts iteration, pulls elements through the entire intermediate chain, and yields a final value that is **not** a `Sequence`. Examples: - Collectors: `toList`, `toSet`, `toMutableList`, `associate`, `groupBy` - Aggregates: `sum`, `count`, `reduce`, `fold`, `maxOrNull`, `average` - Searches / predicates: `first`, `firstOrNull`, `find`, `any`, `all`, `none` - Side effects: `forEach` - Rendering: `joinToString` ### Laziness in action ```kotlin val seq = sequenceOf(1, 2, 3, 4) .map { println("map $it"); it * 2 } // intermediate: prints NOTHING yet .filter { println("filter $it"); it > 4 } // intermediate: prints NOTHING yet println("before terminal") val result = seq.toList() // terminal: NOW the prints happen, element-by-element ``` Until `toList()` runs, no `map`/`filter` lambda executes. The terminal op drives the loop. Note the **element-by-element** order: for each element the whole chain runs before moving to the next (`map 1`, `filter 2`, `map 2`, `filter 4`, ...), unlike eager `Iterable` ops which finish all `map`s before any `filter`. ### Why it matters - **No intermediate allocations**: a sequence chain doesn't build a fresh list per step. - **Short-circuiting**: a terminal like `first` or `take(n)` stops pulling once satisfied, so upstream work for later elements never runs. - **A pipeline with only intermediate ops is inert** — a classic bug is forgetting the terminal and wondering why nothing happened.

  • If I write a sequence chain of only map and filter and never call a terminal op, what happens?
    Nothing. The lambdas never execute because no terminal op drives iteration; the sequence is just a description.
  • Name a terminal op that does not return a collection.
    sum, count, first, any, fold, or forEach — these return a scalar, a boolean, or nothing (Unit).

Intermediate ops write the recipe; the terminal op actually cooks the meal.

saying these in an interview costs you the question

  • Saying map on a sequence immediately produces a list
  • Claiming intermediate ops run when you call them
  • Listing toList as an intermediate op
  • Not knowing a missing terminal means no execution
  • Confusing Sequence laziness with eager Iterable behavior

context