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?
answer
- apply/run/with = this (receiver)
- let/also = it (argument)
- apply/also return object; run/let/with return result
- with takes object as param, not extension
- ?.let for null + naming
basics
~10 sapply, 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 sScope 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 linesdata 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 itgo deeper
Knows apply uses this and returns the object, let uses it and returns the result.
Reproduces the full table and picks the right function for configure vs transform vs side-effect.
Explains receiver-shadowing risks, why ?.let is idiomatic, and that with isn't an extension.
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