skip to content

Compare the signatures of apply, run, with, also and let. Which use a receiver function type, which use a plain function type, and how does that change the lambda body?

level: middleimportance: must knowfreq 75%

answer

  1. apply/run/with = this (receiver)
  2. let/also = it (argument)
  3. apply/also return object; run/let/with return result
  4. with takes object as param, not extension
  5. ?.let for null + naming

basics

~10 s

apply, run and with give you this (receiver lambdas); let and also give you it (plain lambdas). With this you call members directly; with it you must name the argument.

solid answer

~40 s

Scope functions split on two axes: receiver (this) vs argument (it), and return-receiver vs return-lambda-result. apply: T.() -> Unit, returns T (this). run: T.() -> R, returns R (this). with(x): T.() -> R, returns R (this) but takes the object as a parameter. let: (T) -> R, returns R (it). also: (T) -> Unit, returns T (it). The receiver variants (apply/run/with) let you access members unqualified because the lambda is a receiver function type; the it variants (let/also) pass the object as a named parameter, which is clearer when you have nested scopes or need null handling with ?.let. apply/also return the receiver for call-chaining/configuration; run/let/with return the lambda's last expression for transformation.

code

kotlin · 12 lines
kotlin
data class Cfg(var host: String = "", var port: Int = 0)

val cfg = Cfg().apply {   // T.() -> Unit, returns Cfg
    host = "localhost"     // this.host
    port = 8080
}

val url = cfg.run {        // T.() -> R, returns R
    "http://$host:$port"   // uses this.host/port, yields a String
}

val shouted = url.let { it.uppercase() } // (T) -> R via it

go deeper

for a junior

Knows apply uses this and returns the object, let uses it and returns the result.

for a middle

Reproduces the full table and picks the right function for configure vs transform vs side-effect.

for a senior

Explains receiver-shadowing risks, why ?.let is idiomatic, and that with isn't an extension.

for a principal

Advises team conventions for scope-function use and how receiver choice affects DSL ergonomics and readability at scale.

## The two design axes Kotlin's standard scope functions differ along two dimensions: 1. **How the object is exposed**: as the **receiver** (`this`, a receiver function type `T.() -> R`) or as an **argument** (`it`, a plain function type `(T) -> R`). 2. **What is returned**: the **object itself** (`this`) or the **lambda result** (`R`). ## The table | Function | Lambda type | Object as | Returns | |---|---|---|---| | `apply` | `T.() -> Unit` | `this` (receiver) | the object (`this`) | | `run` | `T.() -> R` | `this` (receiver) | lambda result `R` | | `with` | `T.() -> R` | `this` (receiver) | lambda result `R` | | `let` | `(T) -> R` | `it` (argument) | lambda result `R` | | `also` | `(T) -> Unit` | `it` (argument) | the object | Note `with` is **not** an extension; it takes the receiver as its first ordinary parameter: `with(x) { ... }`. The others are extensions on `T`. ## How the body changes ```kotlin // receiver (this): members unqualified val p = Person().apply { name = "Ann"; age = 30 } // configure, return Person val len = "hello".run { length * 2 } // 'this' is the String -> 10 // argument (it): must name it val up = "hello".let { it.uppercase() } // transform, return result person?.also { log(it.name) } // side effect, return person ``` ## Choosing between them - Configuring an object then returning it: **`apply`** (receiver) or **`also`** (argument, good for logging/side effects). - Transforming into something else: **`run`**/**`with`** (receiver) or **`let`** (argument). - Null-safety: **`?.let { it... }`** is idiomatic because `it` reads clearly and avoids shadowing `this`. ## Why receiver vs it matters in nesting With `it` you can rename the parameter (`let { user -> ... }`) to avoid ambiguity in nested blocks; with `this` deeper receivers can shadow outer ones, which is why DSLs sometimes add `@DslMarker`.

  • Why is ?.let preferred over ?.run for null-safe blocks?
    Both work, but let exposes the value as it, which reads clearly and lets you rename it; run hides it in this, which can shadow an enclosing receiver.
  • Why isn't with an extension function?
    with is a top-level function taking the object as its first parameter (with(x){}) for readability when you aren't chaining; the receiver still becomes this via its T.() -> R lambda.

Receiver functions hand you the steering wheel (this); argument functions hand you the keys with a label (it).

saying these in an interview costs you the question

  • Saying apply returns the lambda result (it returns the receiver)
  • Saying let exposes this (it exposes it)
  • Claiming with is an extension function
  • Mixing up which functions return the object vs the result

context