skip to content

You have `user?.apply { ... }` and `user?.also { ... }`. How do these differ from `let`/`run` for nullable handling, and why are they usually NOT the right choice for transform-or-default?

level: middleimportance: should knowfreq 45%

answer

  1. apply/also return the object
  2. let/run return the lambda result
  3. apply -> this, also -> it
  4. also = side effect mid-chain
  5. apply = configure-and-return

basics

~20 s

apply and also run their block only when the value is non-null, but they return the object itself, not the block's result. So they are for side effects (logging, configuring), not for producing a transformed value or a default.

solid answer

~40 s

`apply` and `also` return the **context object**, not the lambda result. `apply` exposes it as `this` (receiver); `also` exposes it as `it` (argument). With `?.`, the block runs only when the value is non-null. Because they return the object, the `?.apply{} ?: default` pattern returns the *object or the default* — useful for 'configure-if-present, else fallback object', but they cannot map the value into a different type. For **transform-or-default** you want `let`/`run`, which return what the block computes. `also` shines for **side effects in a chain** (logging, validation) where you must keep passing the original value along; `apply` shines for **configuring** a freshly created or existing object. Misusing `apply`/`also` for transformation is a common smell — the result is the object, so callers get the wrong thing.

code

kotlin · 5 lines
kotlin
// also keeps the original value flowing while logging
val id: Int? = token
    ?.also { println("validating $it") }
    ?.takeIf { it.isNotBlank() }
    ?.let { it.hashCode() }   // only let actually transforms the type

go deeper

for a junior

Knows apply/also return the object and run only when non-null.

for a middle

Correctly picks let/run for transform-or-default and reserves apply/also for side effects/configuration.

for a senior

Uses also for mid-chain validation/logging and apply for builder-style config, keeping chains type-correct.

for a principal

Codifies 'object-returning vs result-returning' as a review heuristic and spots transform-via-apply as a latent bug.

## Return value is the deciding factor The four 'lambda-result' vs. 'object' scope functions: | Function | Object as | Returns | |----------|-----------|---------| | `let` | `it` | lambda result | | `run` | `this`| lambda result | | `apply` | `this`| **the object** | | `also` | `it` | **the object** | `apply` and `also` always hand back the **same object** you started with. That makes them ideal for **side effects**, not transformations. ## With null safety ```kotlin val cfg = config?.apply { timeout = 30 } ?: Config.DEFAULT // returns the configured object or default val u = user?.also { log.info("got $it") } // returns user (nullable), after logging ``` With `?.`, the block runs only when non-null. But note: - `config?.apply { ... }` returns the **non-null configured object** (or null → Elvis fallback). Good when you want 'tweak-and-return-the-thing'. - `config?.let { it.timeout = 30; it.summary() }` returns the **summary**, a different value. ## Why apply/also are wrong for transform-or-default Transform-or-default means: turn `T?` into some `R`. `apply`/`also` can only return `T` (the object), so you literally cannot express the mapping; you'd be forced to mutate and read separately, which is less clear and only works for mutable types. ## Good uses ```kotlin // also: side effect mid-chain, keep the value flowing val parsed = raw?.trim()?.also { require(it.isNotEmpty()) }?.toInt() // apply: configure then return the object val req = Request()?.apply { headers["Auth"] = token } // builder-style ``` ## Mental rule - Need the **transformed value / default** → `let` or `run`. - Need to **do something and keep the object** → `also` (with `it`) or `apply` (with `this`).

  • What does `user?.apply { name = "x" } ?: defaultUser` return?
    The same user object (now with name set) if user was non-null, otherwise defaultUser. It returns the object, not a derived value.
  • Why is `also` preferred over `let` for logging in a chain?
    also returns the original object, so the chain continues with the unchanged value; let would return whatever the log call returns (often Unit), breaking the chain.

let/run are a juicer (you get juice out); apply/also are a wash station (you get the same apple back, just cleaned or labeled).

saying these in an interview costs you the question

  • Saying apply/also return the lambda result
  • Using apply to 'transform' a value into another type
  • Confusing apply (this) with also (it)
  • Thinking also can change the type that flows down the chain
  • Not knowing the block is skipped when the receiver is null

context