skip to content

Given a chain mixing apply and also with a non-Unit last line in the block, what value flows out? Trace it and explain.

level: seniorimportance: should knowfreq 45%

answer

  1. apply/also discard block result, return receiver
  2. Block type is (T)->Unit / T.()->Unit, last expr ignored
  3. let/run return the last expression instead
  4. Pin value back to receiver at each boundary
  5. Non-Unit last line compiles silently

basics

~10 s

No matter what the last line of an apply or also block evaluates to, the chain keeps flowing the original object forward. The block's result is discarded; only the receiver comes out.

solid answer

~40 s

Both apply and also discard the lambda's result and return the receiver. So even if the last statement in the block is a non-Unit expression — say add() returning a Boolean, or a property read — that value is thrown away and the original object continues down the chain. This is the key contrast with let/run, which return the lambda's last expression. A common trap: a candidate expects the Boolean from list.apply { add(x) } or treats also like let. Tracing rule: at each apply/also boundary, mentally pin the value back to the receiver. Because the block type is (T) -> Unit (also) or T.() -> Unit (apply), the compiler doesn't even require a Unit-typed last line; it just ignores whatever it produces.

code

kotlin · 5 lines
kotlin
val list = mutableListOf(1)
    .apply { add(2) }   // Boolean ignored -> list
    .also { it.add(3) } // Boolean ignored -> list
    .apply { sum() }    // Int ignored -> list
println(list) // [1, 2, 3]  (a List, never Boolean/Int)

go deeper

for a junior

Recognizes apply/also return the object but may be unsure why a non-Unit last line compiles.

for a middle

Correctly traces that the receiver flows out and contrasts with let/run.

for a senior

Explains the (T)->Unit block signature and the implicit-discard rule, and predicts the type after swapping to let.

for a principal

Articulates the design rationale (pass-through vs transform) and the readability/foot-gun trade-off of silent discards in chains.

## The rule, precisely The block parameter types are: - `apply`: `block: T.() -> Unit` - `also`: `block: (T) -> Unit` The declared return of the **block** is `Unit`, but Kotlin allows the last expression of a `Unit`-returning lambda to be any type — it's simply **coerced/ignored**. The **function** `apply`/`also` then returns `T` (the receiver). So: > At every `apply` or `also`, the value flowing out is the receiver, regardless of the block's last expression. ## Worked trace ```kotlin val out = mutableListOf("a") .apply { add("b") } // add returns Boolean(true) -> IGNORED; out value = the list [a, b] .also { it.add("c") } // add returns Boolean -> IGNORED; value = same list [a, b, c] .apply { size } // size is Int -> IGNORED; value = same list // out : MutableList<String> = [a, b, c] ``` Every boundary pins the value back to the list. `out` is the `MutableList`, never a `Boolean` or `Int`. ## Contrast with let / run (so the difference is sharp) ```kotlin val n = mutableListOf("a") .let { it.add("b") } // let RETURNS the lambda result -> Boolean true // n : Boolean = true (the list is gone from the chain) val m = mutableListOf("a") .run { add("b"); size } // run returns last expression -> Int // m : Int ``` `let`/`run` *transform*; `apply`/`also` *pass through*. ## Why the language allows a non-Unit last line Kotlin permits any last expression in a lambda whose expected return type is `Unit` — it's an implicit "discard." That's why `apply { add(x) }` compiles even though `add` returns `Boolean`. There's no warning by default, which is exactly why this trips people up. ## Practical guidance - If you *want* the last expression's value, you chose the wrong function — use `let` or `run`. - If you want the object back no matter what, `apply`/`also` are correct and you can safely end the block on any expression. - Beware accidentally relying on a value from inside an apply/also block — it never escapes.

  • Change the first .apply to .let — what is the chain's type after it?
    Boolean. let returns the lambda result (add returns Boolean), so the list no longer flows; subsequent .also would be on a Boolean.
  • Does the compiler warn that add()'s Boolean is discarded inside apply?
    No. The block's expected return is Unit, so the last expression is implicitly discarded with no default warning — a common source of confusion.

saying these in an interview costs you the question

  • Believing list.apply { add(x) } returns the Boolean from add
  • Treating also/apply as transforming functions like let/run
  • Expecting a value computed inside apply/also to escape the block
  • Not knowing the block's last expression is ignored

context