skip to content

How does the expected return type of a higher-order function affect what the last expression of a multi-statement lambda must be? Contrast forEach (Unit) with map (R).

level: seniorimportance: should knowfreq 44%

answer

  1. forEach is (T)->Unit: last line discarded
  2. map is (T)->R: last line type = R
  3. apply/also return receiver, ignore body result
  4. let/run/with return last expression
  5. Bad last line -> List<Unit> bug

basics

~20 s

If the function expects a Unit-returning lambda (like forEach), Kotlin ignores whatever the last line produces. If it expects a real value (like map), the last expression must produce that value and its type drives the result type.

solid answer

~40 s

The lambda's declared functional type determines how the last expression is treated. `forEach` takes `(T) -> Unit`, so the lambda is **Unit-coerced**: the last expression's value is silently discarded, even if it produces a non-Unit value. `map` takes `(T) -> R` with `R` inferred, so the **last expression's type becomes R** and is kept for every element. This is why an accidental non-Unit last line is harmless in `forEach` but meaningful in `map`. The same Unit-coercion applies to scope functions: `apply`/`also` ignore the lambda result (they return the receiver), while `let`/`run`/`with` return the last expression's value. Misplacing a side-effecting expression can silently change `map`'s element type to `Unit` or `Boolean`, a classic refactoring bug.

code

kotlin · 6 lines
kotlin
// Unit-coerced: compiles, value ignored
listOf(1,2).forEach { it + 1 }

// Value-returning: last expr type matters
val result: List<Boolean> = listOf(1,2).map { it > 1 }  // List<Boolean>
val bug: List<Unit> = listOf(1,2).map { println(it) }   // List<Unit>!

go deeper

for a junior

Knows map keeps the last value and forEach does not return one.

for a middle

Explains Unit-coercion and that map's element type comes from the last expression.

for a senior

Distinguishes apply/also (receiver) from let/run/with (value) and predicts the List<Unit> refactoring bug.

for a principal

Sets conventions to prevent silent type drift (forEach for effects, lint, explicit return types) and reasons about inference brittleness in large lambdas.

## Functional types drive the rule Every lambda implements a **functional type** such as `(T) -> R`. The expected `R` controls what the last expression must yield. ### Unit-coercion (forEach, apply, also) When `R == Unit`, Kotlin applies **Unit conversion**: the lambda compiles even if its last expression is non-Unit; that value is just thrown away. ```kotlin listOf(1, 2, 3).forEach { it * 100 // non-Unit last expression — allowed, value discarded } ``` `apply` and `also` are similar: they always return the **receiver**, so the lambda's last expression is ignored. ```kotlin val sb = StringBuilder().apply { append("x") length // ignored; apply returns the StringBuilder } ``` ### Value-returning (map, let, run, with) When `R` is a real type, the **last expression's type is inferred as R** and used: ```kotlin val lens: List<Int> = listOf("a","bb").map { it.length // R = Int } val r: String = something.let { it.toString() // run/let/with return the last expression } ``` ## The classic refactoring bug Reordering or appending a side-effecting line can silently change the element type: ```kotlin val ok: List<Int> = list.map { val n = it.length n // R = Int } val oops: List<Unit> = list.map { val n = it.length log(n) // log returns Unit -> R = Unit ! } ``` `oops` is now `List<Unit>`, often surfacing as a confusing downstream type error rather than at the lambda itself. ## Practical guidance - For pure side effects, prefer `forEach` so Unit-coercion makes the intent explicit and the last line can't accidentally matter. - For transformations, ensure the last expression is exactly the value you want and check the inferred element type. - Remember `apply`/`also` ignore the result; `let`/`run`/`with` keep it — a frequent source of 'why is my value the receiver?' confusion.

  • Why does apply ignore the lambda's last expression?
    apply is defined to return its receiver (this), not the lambda result; its block type is T.() -> Unit, so the body is Unit-coerced.
  • How can a stray println turn map into List<Unit> without an obvious error?
    If println is the last expression, R is inferred as Unit. The map call itself compiles; the type error only appears where the list is later used as List<Something-else>.

forEach is a one-way mailbox — you drop something in and it's gone; map is a vending machine — whatever you put on the last shelf is what comes out.

saying these in an interview costs you the question

  • Saying forEach's last expression becomes the result
  • Not knowing apply/also ignore the lambda body's value
  • Thinking map can't accidentally produce List<Unit>
  • Confusing let (returns value) with also (returns receiver)
  • Believing Unit-coercion is an error rather than allowed

context