skip to content

Compare apply with let, run, with, and also: which provide a receiver (this) vs an argument (it), and which return the receiver vs the lambda result?

level: middleimportance: must knowfreq 72%

answer

  1. Axis 1: this (apply/run/with) vs it (let/also)
  2. Axis 2: returns receiver (apply/also) vs result (let/run/with)
  3. apply = configure + return object
  4. with is a function, not extension; no ?.
  5. ?.let for null-safe transform

basics

~20 s

apply and run use 'this'; let and also use 'it'. apply and also give you the object back; let and run give you the lambda's result. with is like run but takes the object as an argument.

solid answer

~40 s

The five scope functions vary on two axes. Context object: apply, run, with use the object as receiver (this); let, also pass it as the argument it. Return value: apply and also return the receiver object (good for chaining/configuring); let, run, with return the lambda result (good for transforming/computing). So apply = this + returns receiver (configure in place); also = it + returns receiver (side effects); run = this + returns result (compute on receiver); let = it + returns result (transform, null-safe with ?.let); with = this + returns result but is a regular function with(obj) { ... }, not an extension. For the builder idiom you want this (terse member access) and the object back, which is exactly apply.

go deeper

for a junior

Can name that apply uses this and returns the object, even if fuzzy on the others.

for a middle

Correctly fills the full 5-function matrix on both axes and picks apply for the builder case.

for a senior

Explains the extension-vs-function nuance (with, run's two forms) and chains apply/also deliberately.

for a principal

Advises on team conventions: when overuse of scope functions harms readability and how to standardize their intent.

## The two axes Every scope function combines two independent choices: 1. **How the object is exposed** — as the *receiver* `this` (implicit, terse member access) or as the *argument* `it` (explicit, renameable). 2. **What the function returns** — the *receiver object* itself, or the *result of the lambda*. ## The matrix | Function | Object as | Returns | Typical use | |----------|-----------|---------|-------------| | `apply` | `this` | receiver | **configure** an object, return it (builder idiom) | | `also` | `it` | receiver | **side effect** that reads the object (log, validate) | | `run` | `this` | lambda result | **compute** something from the object | | `let` | `it` | lambda result | **transform**; null-safe with `?.let { }` | | `with` | `this` | lambda result | like `run` but a plain function: `with(obj) { }` | ## Signatures ```kotlin inline fun <T> T.apply(block: T.() -> Unit): T // this, returns T inline fun <T> T.also(block: (T) -> Unit): T // it, returns T inline fun <T, R> T.run(block: T.() -> R): R // this, returns R inline fun <T, R> T.let(block: (T) -> R): R // it, returns R inline fun <T, R> with(receiver: T, block: T.() -> R): R // this, returns R (not an extension) ``` Note `with` is not an extension function — it takes the object as its first argument, so you can't chain it with `?.`. `run` has an extension form (`obj.run { }`) and a no-receiver form (`run { }`) for grouping statements. ## Choosing for the builder idiom For configure-and-return you want **member access via `this`** (so `url = u` works) **and the object back** (to use the expression as the result). Only `apply` gives both. `run` uses `this` but returns the lambda result; `also` returns the object but exposes it as `it`, making configuration noisier (`it.url = u`). ```kotlin // Idiomatic: configure with apply val conn = Connection().apply { timeout = 30; retries = 3 } // also for the side effect afterwards val conn2 = Connection() .apply { timeout = 30 } .also { logger.info("created $it") } ``` ## Common pitfall Using `apply` when you actually want a transformed value (you then have to reference a mutated property), or using `let`/`run` and accidentally returning the wrong thing because the last expression became the result. Returning-the-receiver (`apply`/`also`) is precisely what makes the result predictable for builders.

  • Why can't you write obj?.with { } the way you write obj?.apply { }?
    with is not an extension function — the object is a normal parameter, so there's no receiver to apply the safe-call ?. to. apply/let/run/also are extensions and chain with ?.
  • You need to log an object right after configuring it without breaking the chain. Which functions and why?
    apply to configure (returns the object), then also to log it (it gives you the object, returns it unchanged): obj.apply { ... }.also { log(it) }.

saying these in an interview costs you the question

  • Saying apply and run differ only in syntax (ignoring return value)
  • Claiming with returns the receiver (it returns the lambda result)
  • Thinking let exposes this rather than it
  • Saying all five are interchangeable
  • Not knowing with is a non-extension function

context