skip to content

Compare `?.let` with `?.also`, `?.run`, and `?.apply` for null-guarded code. When would you pick each, and what does each return?

level: seniorimportance: should knowfreq 55%

answer

  1. let/also -> `it`; run/apply -> `this`
  2. let/run -> lambda result; also/apply -> receiver
  3. let = transform, also = side effect
  4. apply = configure, run = compute via members
  5. Nesting? prefer it-based let/also to avoid `this` clash

basics

~20 s

All four run only on non-null receivers. let/also pass the value as it; run/apply expose it as this. let/run return the block's result; also/apply return the original receiver. Pick by what you need bound and what you want back.

solid answer

~40 s

The scope functions differ on two axes — **how the receiver is referenced** and **what is returned**. `let`: argument `it`, returns lambda result — best for transforming a nullable into something else. `run`: receiver `this`, returns lambda result — like `let` but when you want member access without `it.`. `also`: argument `it`, returns the receiver — for side effects (logging, validation) inside a chain. `apply`: receiver `this`, returns the receiver — for configuring/mutating an object. With `?.`, all skip the block on null. Choose `let` to map a value, `also` to peek/log without breaking the chain, `apply` to set properties on a freshly created nullable, `run` to compute a result using member calls. Overusing `this`-based ones (`run`/`apply`) in nested blocks causes `this` ambiguity, so prefer the `it`-based `let`/`also` when nesting.

go deeper

for a junior

Knows let exists; likely unsure of the differences among the four.

for a middle

Can state the it/this and return-value table and pick let vs apply correctly.

for a senior

Explains nesting/this-shadowing pitfalls and how also/apply change Elvis-fallback semantics; chooses idiomatically.

for a principal

Drives team conventions to limit scope-function soup and reasons about readability/debuggability of long chains.

## The two axes Kotlin's standard **scope functions** vary along: 1. **Context object reference:** `this` (receiver) vs `it` (argument). 2. **Return value:** the **lambda result** vs the **context object** itself. | Function | Object as | Returns | |----------|-----------|---------| | `let` | `it` | lambda result | | `run` | `this` | lambda result | | `also` | `it` | the object | | `apply` | `this` | the object | All are extension functions, so `?.let`, `?.run`, `?.also`, `?.apply` all **skip the block when the receiver is null** (the safe call short-circuits). ## Picking the right one ```kotlin // let: transform a nullable into another value val len: Int? = name?.let { it.trim().length } // run: like let but use members directly via `this` val area: Int? = rect?.run { width * height } // also: side effect, keep the value flowing val saved = user?.also { log.info("saving ${it.id}") }?.let { repo.save(it) } // apply: configure/mutate, return the same object val intent = maybeIntent?.apply { putExtra("k", 1) flags = 0 } ``` - **`let`** — mapping/transforming, or running code only on non-null and returning a new value. - **`run`** — same intent as `let` but you want member access without the `it.` prefix; also has a no-receiver form `run { }`. - **`also`** — "do this on the side" (logging, validation, debugging) while passing the **original** object onward; great in chains. - **`apply`** — builder-style configuration of an object you then return (common with Android `Intent`/`Bundle`, or any DTO setup). ## `this` vs `it` and nesting `run`/`apply` shadow `this`, so **nested** `apply { apply { } }` makes `this` ambiguous and bugs hide easily. The `it`-based `let`/`also` keep distinct names (and you can rename `it` to `user ->`), so prefer them when nesting null-guarded blocks. ## Return-value pitfall Because `also`/`apply` return the **receiver**, chaining Elvis after them behaves differently than after `let`/`run`. `user?.also { ... } ?: fallback` falls back only when `user` is null (the receiver is returned), whereas `user?.let { f(it) } ?: fallback` also falls back when `f(it)` is null. This is a frequent source of subtle bugs. ## Keywords/APIs named `let`, `run`, `also`, `apply` (and `with`, the non-extension cousin), `this`, `it`, `?.`, Elvis `?:`, scope functions.

  • Why does `x?.also { } ?: y` differ from `x?.let { } ?: y` regarding the fallback?
    `also` returns the original `x`, so Elvis falls back only when `x` is null. `let` returns the lambda result, so it also falls back when that result is null.
  • When would you choose `apply` over `let`?
    When you want to mutate/configure the object and get the **same object** back (builder style), not transform it into a different value.

saying these in an interview costs you the question

  • Saying all four return the same thing
  • Mixing up `it` (let/also) and `this` (run/apply)
  • Claiming `apply` returns the lambda result
  • Not realizing `also`/`apply` change Elvis-fallback semantics
  • Using `apply` for transformation where `let` is correct

context