skip to content

What are the two axes that distinguish Kotlin's five scope functions (let, run, with, apply, also), and how do they map to choosing one?

level: juniorimportance: must knowfreq 75%

answer

  1. Two axes: this vs it, and result vs original
  2. apply=this+self, also=it+self
  3. let=it+result, run/with=this+result
  4. self-returners are for configuring/chaining
  5. with is not an extension (no ?. chaining)

basics

~20 s

Two things differ: how you refer to the object inside the block (as 'this' or as 'it'), and what the block gives back (the block's last line, or the original object). Pick based on those two needs.

solid answer

~40 s

Kotlin's scope functions vary on two axes. Axis 1 — context object reference: run/with/apply expose it as the receiver this (you can drop the name and call members directly); let/also expose it as the lambda argument it (an explicit, renamable parameter). Axis 2 — return value: let/run/with return the lambda result (the last expression); apply/also return the original object (for chaining/configuring). So: apply = this + original object (configure-and-return); also = it + original object (side effects); run/with = this + result (compute on a receiver); let = it + result (transform/null-safe map). Choose by asking: do I want a clean receiver (this) or an explicit name (it), and do I want a computed result or the same object back?

code

kotlin · 6 lines
kotlin
val numbers = mutableListOf(1, 2, 3)
val result = numbers
    .also { println("before: $it") }   // it + original -> still the list
    .apply { add(4) }                  // this + original -> still the list
    .let { it.sum() }                  // it + result -> Int
println(result) // 10

go deeper

for a junior

Should name the two axes and correctly place at least apply and let on them.

for a middle

Maps all five precisely and picks one for a given snippet with justification.

for a senior

Explains the receiver-lambda type (T.() -> R) vs (T) -> R and the with-is-not-an-extension nuance.

for a principal

Frames the choice as a readability/intent convention and can codify team guidelines (e.g. apply only for config, also only for side effects).

## What scope functions are Kotlin's standard library provides five **scope functions** — `let`, `run`, `with`, `apply`, and `also` — declared in `Standard.kt`. They all do one thing: execute a block (lambda) **in the context of an object**, giving you a temporary scope to operate on that object. They differ only in *how the object is exposed* and *what is returned*. Choosing the right one is purely a readability decision; functionally you can often achieve the same effect with any of them. ## Axis 1 — Context object: `this` (receiver) vs `it` (argument) - **Receiver (`this`)** — used by `run`, `with`, `apply`. The lambda is a *function literal with receiver* (`T.() -> R`). Inside, the object **is** `this`, so you can call its members without any qualifier: `name` instead of `it.name`. You may still write `this` explicitly. Best when you make many member calls. - **Argument (`it`)** — used by `let`, `also`. The lambda is an ordinary `(T) -> R`. The object is the single parameter, named `it` by default, but you can rename it: `user.let { u -> ... }`. Best when you pass the object to other functions (`it`), when you want to rename it for clarity, or when nesting scopes (to avoid `this` shadowing). ## Axis 2 — Return value: lambda result vs original object - **Lambda result** — `let`, `run`, `with` return whatever the block's **last expression** evaluates to. Use them to **transform** or **compute** a value. - **Original object** — `apply`, `also` return the **same object** they were called on, ignoring the block's result. Use them to **configure** an object or run **side effects** while keeping the object in a chain. ## The full mapping | Function | Object as | Returns | Typical use | |----------|-----------|---------|-------------| | `let` | `it` | lambda result | transform value; null-safe `?.let` | | `run` | `this` | lambda result | compute on a receiver; run a block | | `with` | `this` | lambda result | group calls on a non-null object (not an extension) | | `apply` | `this` | original object | configure/build then return it | | `also` | `it` | original object | side effects (log, validate) in a chain | ```kotlin data class User(var name: String = "", var age: Int = 0) val user = User().apply { // this + original -> configure & return User name = "Ada" age = 30 }.also { // it + original -> side effect, still User println("created $it") } val label: String = user.let { // it + result -> transform to String "${it.name} (${it.age})" } val summary = with(user) { // this + result -> compute using members "$name is $age" } ``` ## How to choose, mechanically 1. Do I want the **same object back** (chaining/config)? → `apply` or `also`. Otherwise a **computed result** → `let`, `run`, or `with`. 2. Do I prefer a **clean receiver** (`this`, many member calls) or an **explicit name** (`it`, passing it around / nesting)? → `apply`/`run`/`with` for `this`; `let`/`also` for `it`. Note `with(x){}` is a regular function taking the object as an argument (so it doesn't help with null-safe `?.`), whereas `run` is also available as an *extension* `x.run{}` that does chain with `?.`.

  • If apply and run both use this, what is the single difference?
    Return value: apply returns the original object; run returns the lambda's last expression.
  • Why might you prefer let over run even though run is shorter?
    let exposes the object as a renamable it, which is clearer when nesting scopes or passing the object to another function as an argument.

Think of a workshop bench: 'this' hands you the object already mounted on the bench (call its tools directly); 'it' hands you the object in a labeled box you can pass around.

saying these in an interview costs you the question

  • Claiming all five are interchangeable with no semantic difference
  • Saying apply returns the lambda result
  • Thinking it cannot be renamed
  • Believing this and it are the same thing under the hood
  • Not knowing with returns the lambda result, not the object

context