skip to content

How does let behave when chained on collections or in transformation pipelines, and how is it different from map?

level: seniorimportance: should knowfreq 45%

answer

  1. let: whole object as it, one result; map: per element
  2. Use let to name an intermediate / terminate a chain
  3. inline => no allocation when chaining
  4. Prefer process(x) over x.let { process(it) } with no benefit
  5. On Sequence, let acts on the Sequence object itself

basics

~20 s

let treats the whole value as one thing: it passes the entire object in as it and returns one result. map transforms each element of a collection. So list.let acts on the list itself; list.map acts on items.

solid answer

~50 s

`let` operates on its **receiver as a whole single value** and returns a single lambda result. When you call `list.let { it.size }`, `it` is the whole `List`, not an element, and the result is one `Int`. `map`, by contrast, is a collection operation that applies a transform to **each element** and returns a new collection. They compose: `list.map { it * 2 }.let { doubled -> doubled.sum() }` — `map` transforms elements, then `let` takes the resulting list as a whole to fold it into one value, while giving that intermediate list a readable name. `let` is therefore a great 'pipeline terminator' or 'name this intermediate result' step. Because `let` is `inline`, chaining it adds no lambda allocation. Note: don't reach for `let` where a method reference or direct call is clearer — `value.let(::process)` is fine but `process(value)` is plainer.

code

kotlin · 8 lines
kotlin
val orders = listOf(120, 0, 80)
val summary = orders
    .filter { it > 0 }            // [120, 80]
    .map { it * 1.2 }            // [144.0, 96.0]  per-element
    .let { totals ->             // totals = the whole list
        "count=${totals.size}, sum=${totals.sum()}"
    }
println(summary) // count=2, sum=240.0

go deeper

for a junior

Knows let acts on the whole value and map acts on elements.

for a middle

Can compose map then let and explain the differing it meaning.

for a senior

Uses let as a named pipeline step/terminator, notes inline (no allocation), and knows the Sequence nuance.

for a principal

Sets readability conventions for when let adds value in pipelines vs. when intermediate vals or plain calls are clearer.

## The key distinction - **`let`**: receiver-as-a-whole in (`it` = the entire object), single result out. Signature `fun <T, R> T.let(block: (T) -> R): R`. - **`map`**: per-element transform on an `Iterable`/`Sequence`, returns a new collection. Signature roughly `fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R>`. So on a list, `it` inside `let` is the **list**; inside `map` the parameter is an **element**. ```kotlin val xs = listOf(1, 2, 3) xs.let { it.size } // 3 (it = the whole list) xs.map { it * 2 } // [2, 4, 6] (it = each element) ``` ## Why chain let into a pipeline `let` shines as a **named pipeline step** or a **terminator** that collapses a collection into one value while keeping the expression flat and readable: ```kotlin val report = orders .filter { it.paid } .map { it.total } .let { totals -> Report(count = totals.size, sum = totals.sum()) } ``` Naming the intermediate (`totals`) avoids re-typing the chain or introducing a separate `val`. ## Composition, not competition They are not alternatives — `map` builds a new collection; `let` wraps/finishes. Use them together. Because `let` is **`inline`**, inserting it in a chain costs nothing at runtime (no extra lambda object, no extra collection). ## When NOT to use let in a chain - Pure function application: prefer `process(value)` over `value.let { process(it) }` / `value.let(::process)` when there's no naming/null benefit. - Avoid stacking many `let`s; it can hurt readability versus intermediate `val`s. ## Sequences On a `Sequence`, `map` stays lazy; a trailing `let` runs once on the *Sequence object* (still lazy) unless you terminate (e.g., `toList()`), so place `let` after a terminal operation when you want it to act on materialized data.

  • Inside list.let { }, what is the type of it?
    The list type itself (e.g., List<Int>), because let passes the receiver as a whole — not an element.
  • Does inserting a let into a chain allocate an extra object?
    No. let is inline, so the lambda is inlined; there is no extra lambda instance and no intermediate collection from let itself.

map is editing every page of a book; let is doing one thing with the whole book.

saying these in an interview costs you the question

  • Thinking let iterates elements like map
  • Claiming let returns a collection
  • Wrapping a plain function call in let with no naming/null benefit
  • Assuming a trailing let forces a lazy Sequence to materialize

context