# Scope functions

Source: https://codewiki.com/kotlin/scope-functions/

> - **what**: Kotlin's five scope functions immediately invoke a lambda with an object as context. They differ in whether the block calls that object `this` or receives it as an argument, and whether the whole expression returns the object or the lambda result.
> - **trap**: The function name doesn't constrain side effects, and nested implicit receivers can shadow one another. Rules of thumb such as "`let` handles nulls" and "`also` logs" aren't enough for code review.
> - **fix**: Decide what the next link needs returned, then choose between `this` and an explicit parameter for clarity. Write `?.` for nullable receivers, and give nested receivers names or labels.

## What it is and why it exists

A scope function
is one of a group of inline functions in the Kotlin standard library that take an object and a lambda.
They supply that object as temporary context while invoking the lambda, so a small group of adjacent operations needn't repeat its name.
The five functions are `let`, `run`, `with`, `apply`, and `also`.

These functions add no new language capability.
You can write the same assignments, calls, and transformations without them.
They offer a local form for "configure this object," "compute a result from this value," or "observe this value inside a chain."

Two dimensions determine the semantics.
First, the context object is either `this` in the lambda or a regular lambda argument. Second, the call returns either that context object or the lambda's final result.
Once you answer those questions, you don't have to memorize five similar names.

You'll see scope functions in object initialization, nullable transformations, collection processing, and builder calls.
A short block can keep its main object prominent, but a long chain or several nested blocks hide the current type, receiver, and effects.
Plain local variables and sequential statements are still idiomatic Kotlin when they're clearer.

`let` has no special null-safety power.
The safe call `?.` is what skips an invocation, so `value?.run {}`, `value?.apply {}`, and `value?.also {}` also execute only for a non-null value.
Conversely, `nullable.let {}` without `?.` always runs, and `it` may still be `null`.

## How it works

### Two selection axes

The table is the most reliable place to start.
"Argument" means the lambda receives an ordinary parameter named `it` by default. "Receiver" means the lambda is a lambda with receiver, whose members are available through implicit `this`.

| Function | Object inside the block | Whole expression returns | Call form |
| --- | --- | --- | --- |
| `let` | argument `it` | lambda result | extension function |
| `run` | receiver `this` | lambda result | extension function |
| `with` | receiver `this` | lambda result | regular function, `with(value) {}` |
| `apply` | receiver `this` | context object | extension function |
| `also` | argument `it` | context object | extension function |

`let` and `also` pass the object to a lambda shaped like `(T) -> ...`.
A one-parameter lambda may omit its parameter declaration and use `it`, or rename it to a domain term such as `customer` or `shipment`.
Once a block has other values or nested lambdas, a named parameter is usually easier to inspect than successive uses of `it`.

`run`, `with`, and `apply` accept a lambda shaped like `T.() -> ...`.
Such a block makes `T` an implicit receiver, so unqualified property and member calls can resolve against `this`.
That suits a short block that mainly uses the object's members, but it can also obscure resolution among locals, outer receivers, and members.

### The return controls the next link

`let`, `run`, and `with` return the lambda result.
That result is normally the value of the block's last expression, which makes these functions useful for transformations, summaries, or a multi-statement expression.
If the block returns `null`, so does the call; the function doesn't distinguish "receiver was null" from "calculation returned null."

`apply` and `also` ignore the block's result and return the original context object.
For a reference type, the next link receives the same instance, not a copy.
The receiver form of `apply` suits concentrated member configuration. The argument form of `also` suits passing an object to logging, validation, or registration code while keeping the original value in the chain.

Returning the context object doesn't mean the block left it unchanged.
`also` may freely mutate a mutable object through `it`, and `apply` may perform I/O. Their names are reading conventions, not an effect system enforced by the compiler.
Review the calls inside the block instead of inferring purity from the function name.

### `with` and the two forms of `run`

`with(value) { ... }` isn't an extension call. It passes `value` as the first argument to a regular function.
The block always executes, and if `value` has a nullable type, `this` may be `null` inside it.
When null should skip the block, `value?.run { ... }` usually states the condition more directly.

The standard library also has `run { ... }` without a context object.
It invokes a `() -> R` and returns the result, which is useful for introducing a few locals where an expression is required.
It isn't a sixth object scope function and supplies no implicit `this`; don't conflate the two `run` call shapes.

Extension `run` and `let` both return a calculation.
If the object is mainly an argument to several calls, a named `let` parameter is clearer. If the block mainly invokes its members, the receiver form of `run` is terser.
You can rewrite either as the other, so choose by readability rather than capability.

### Nulls and call placement

Evaluation of `value?.let { transform(it) }` checks the receiver before deciding whether to call `let`.
For a null receiver, the lambda doesn't run and the safe call yields `null`. For a non-null receiver, the block parameter is narrowed to a non-null type.
The same rule applies to the other extension scope functions.

Safe-call placement changes the boundary.
`source?.let { parse(it) }?.also { save(it) }` skips `also` when `source` is null or `parse` returns null.
With `source?.let { parse(it).also { save(it) } }`, whether the inner `also` runs depends only on the static call form of `parse`, and its argument may itself be nullable.

An Elvis operator after a nullable result likewise can't say where the null came from.
If "customer not found" and "customer has no email" require different handling, use explicit branches or a sealed result instead of collapsing both into `?.let { ... } ?: fallback`.

### A selection order

Choose a function in a fixed order:

1. Decide whether the whole expression should return the context object or a new result.
2. Decide whether an implicit receiver or a named parameter makes the block easier to read.
3. If a null receiver should skip an extension call, put `?.` there explicitly.
4. Check nesting, chain length, and effects; return to local variables if the structure is still unclear.

This order is more dependable than memorizing names by use case.
For example, "used for logging" doesn't determine `also` by itself. If the code must return the logging call's result, `let` matches the return shape instead.
Trace the types first and the intent words second.

## Examples

The four programs progress from a receiver-preserving configuration chain to nullable transformation, result calculation, and nested receivers.
Each was compiled with Kotlin 2.4.10 and run on JRE 21; every output block below comes from that execution.

### Configure and keep the same object

`apply` configures `Shipment` members, then `also` records an event and emits audit text.
Both calls return the `Shipment` they received, so the final `shipment` is still the instance just created.

<!-- quick -->

```kotlin
// file: configure_shipment.kt
data class Shipment(
    val id: String,
    var carrier: String = "",
    var insured: Boolean = false,
    val events: MutableList<String> = mutableListOf(),
)

fun main() {
    val shipment = Shipment("S-104").apply {
        carrier = "Rail"
        insured = true
    }.also { configured ->
        configured.events += "configured"
        println("audit: ${configured.id}/${configured.carrier}")
    }

    println(shipment)
}
```

```text
audit: S-104/Rail
Shipment(id=S-104, carrier=Rail, insured=true, events=[configured])
```

<!-- /quick -->

Unqualified assignments in the `apply` block resolve against the `Shipment` receiver.
The named `also` parameter makes ownership of the event list obvious.
The output also proves that `also` isn't read-only: it returns the original object, but the block still adds an event.

If the audit call can throw, the chain won't return `shipment` and later statements won't run.
A scope function doesn't isolate a failed effect. The caller's error policy must decide whether "configured but not audited" is acceptable.

### Transform a nullable value with `let`

The lookup may find no customer, and the email field may also be absent.
Two safe calls guard those boundaries, while the inner `let` turns a normalized email into the final text.

```kotlin
// file: nullable_contact.kt
data class Customer(val id: String, val email: String?)

fun findCustomer(id: String): Customer? = when (id) {
    "C-7" -> Customer(id, " ADA@EXAMPLE.COM ")
    "C-8" -> Customer(id, null)
    else -> null
}

fun contactLine(id: String): String =
    findCustomer(id)?.let { customer ->
        customer.email
            ?.trim()
            ?.lowercase()
            ?.let { email -> "${customer.id} <$email>" }
    } ?: "no contact"

fun main() {
    println(contactLine("C-7"))
    println(contactLine("C-8"))
    println(contactLine("C-9"))
}
```

```text
C-7 <ada@example.com>
no contact
no contact
```

`C-8` and `C-9` produce the same text because both paths make the left side of Elvis `null`.
That is intentional for this function's contract. If callers must distinguish a missing customer from a missing email, preserve both outcomes instead of adding more `let` calls.

The code explicitly names `customer` and `email`.
If both levels used `it`, the inner parameter would shadow the outer one, making it easy for a string template to refer to the wrong object.

### Compute results with `run` and `with`

Extension `run` validates and sums values on the invoice receiver, returning the final `Int`.
`with` then supplies that invoice as a receiver and returns a formatted string.

```kotlin
// file: invoice_summary.kt
data class Invoice(val number: String, val lineCents: List<Int>)

fun Invoice.totalCents(): Int = run {
    require(lineCents.isNotEmpty()) { "invoice must have lines" }
    lineCents.sum()
}

fun main() {
    val invoice = Invoice("INV-9", listOf(450, 325, 125))
    val summary = with(invoice) {
        "$number: ${lineCents.size} lines, ${totalCents()}c"
    }

    println(summary)
}
```

```text
INV-9: 3 lines, 900c
```

Both functions return lambda results here, so `totalCents()` is an `Int` and `summary` is a `String`.
If `run` were mistakenly changed to `apply`, the extension would try to return an `Invoice` rather than its declared `Int`, and the compiler would report a type mismatch.

`with` works well when you already have a non-null object, want to group member reads, and need a result.
It provides no null short-circuit, so don't rewrite `invoice?.run` as `with(invoice)` merely for stylistic symmetry.

### Label nested receivers

The outer `apply` receiver is a `Form`, while the inner `run` receiver is a `Profile`; both have `city`.
Labels make both assignment target and source explicit so the nearest receiver can't silently take over an unqualified name.

```kotlin
// file: nested_receivers.kt
data class Profile(val city: String)

class Form {
    var city: String = "unset"
    fun render(): String = "city=$city"
}

fun buildForm(profile: Profile): Form =
    Form().apply formScope@{
        profile.run profileScope@{
            this@formScope.city = this@profileScope.city
        }
    }

fun main() {
    println(buildForm(Profile("Paris")).render())
}
```

```text
city=Paris
```

Without labels, bare `city` in the inner block first encounters the nearest `Profile` receiver.
Its property is a `val`, so one mistaken form might fail to compile. If both objects expose writable members with the same name, the more dangerous outcome is code that compiles and updates the wrong object.

Production code can often remove this nesting with a local variable or named helper.
Labels fit short blocks where the receiver relationship itself matters; they shouldn't become an excuse to preserve several layers of scope functions.

## Pitfalls

> **Pitfall:** Memorizing only "`let` is for nulls" and "`apply` is for configuration" ignores the actual return type. Generated refactors are especially prone to replacing a function name while leaving the rest of the chain intact, so later calls land on the lambda result or the wrong object.

**Fix:** write the input and output static type of every link, then filter functions by return behavior first. After the program compiles, still verify identity and effects because the same type doesn't imply the same value.

> **Pitfall:** Treating `nullable.let {}` as null-safe executes the lambda normally, and `it` remains nullable. The short-circuit comes from `?.` in `nullable?.let {}`.

**Fix:** test a null receiver and mark the exact layer guarded by each safe call. When the block intentionally accepts null, keep the ordinary dot and give its argument a name that doesn't imply the invocation is skipped.

> **Pitfall:** Nested `run`, `apply`, or `with` blocks stack implicit receivers. Add lambdas using `it`, and short names can denote entirely different objects. When those objects have members with the same name, the mistake may still compile.

**Fix:** prefer local variables or named functions. When nesting carries real structural meaning, label receivers, name ordinary parameters, and write `this@label` explicitly at assignments.

> **Pitfall:** `also` returns the original object, but it doesn't guarantee that the block only observes it or that a business effect occurs only once. Network retries, repeated callers, or an exception in the block can make logging, message sends, and database writes disagree with the chain's innocent appearance.

**Fix:** put non-repeatable I/O in a named statement with an explicit error policy. If `also` remains, document exception propagation and idempotency, then test effect counts under repeated calls and injected failures.

> **Pitfall:** `let`, `run`, and `with` can all feed a nullable lambda result into an Elvis branch, so a compact chain may erase distinct failure reasons. Several transformations can also make the intermediate type hard to see at the call site.

**Fix:** merge null paths only when callers consider their reasons equivalent. Use explicit branches or a result type when the reason matters. Name the intermediate result and declare its type once a chain performs more than one substantial transformation.

> **Pitfall:** Scope functions are inline, so a bare `return` in the block may be a non-local return that exits the surrounding named function. A reviewer who reads it as "finish this lambda" misses a path where all following code is skipped.

**Fix:** use `return@let`, `return@run`, or another labeled return to finish only the block. Prefer an ordinary condition or named function for complex exit logic. Test both early-match and no-match paths, and identify the return target explicitly.

<!-- deep -->

## Receiver resolution and nested scopes

A receiver lambda doesn't create an arbitrary newly named object.
Its static function type still supplies `this`, and unqualified members resolve under Kotlin's declaration and implicit-receiver rules.
Locals from outside the block remain visible, so a property and local with the same name can make the short form conceal where data comes from.

With nested receivers, the nearest applicable implicit receiver generally has higher priority.
An explicit label selects an outer lambda's `this`, as in `this@formScope`; an ordinary lambda argument can simply be named.
If code repeatedly reaches across two receiver layers, separating the scopes is usually easier to maintain than adding more qualifiers.

Member and extension resolution rules still apply.
When a scope function is itself an extension function, the dot is call syntax; the function doesn't inject a member into the receiver type.
When several extensions and implicit receivers are visible in one block, imports, declaration location, and static types all affect the candidate set.

DSLs often nest receivers deliberately because the hierarchy is part of their model.
They can use `@DslMarker` to restrict outer receivers that shouldn't remain implicitly accessible.
Ordinary application code without that design boundary usually needs only the smaller tool: a local variable.

## Inlining, contracts, and control flow

All five functions are inline in the Kotlin 2.4.10 standard library.
Their bodies call `block(this)`, `this.block()`, or `receiver.block()`, then choose the block result or context object as the return.
That accounts for the comparison table without treating the function names as special syntax.

Their standard-library implementations also declare a `callsInPlace(block, InvocationKind.EXACTLY_ONCE)` contract.
The compiler can use that information when analyzing control-flow facts such as initialization inside the block.
The contract describes the call convention; it isn't runtime surveillance of business effects and doesn't promise that the function surrounding the scope call runs only once.

Inlining permits some control flow to cross the lambda boundary.
A bare `return` can exit the named function around the call, while `return@let` or an explicit label returns only from the current lambda invocation.
Move that code to a non-inline higher-order function and the same bare return may become illegal, so refactors must compile again and cover both control-flow paths.

Don't infer an unmeasured speed claim from `inline`.
Inlining may remove some function objects or indirect calls, but it can also increase code at call sites. The backend and surrounding code determine the resulting machine code.
This topic has no reproducible benchmark, so it describes semantics and implementation shape without performance numbers.

## Designing a legible chain

A chain stays readable only while its reader can answer "what is the current value?"
`apply` and `also` preserve the context value, `let` and `run` switch to the block result, and `with` begins a result computation from an object in parentheses.
Writing the static type of every step on paper or in a review comment often exposes a mistaken substitution immediately.

A scope-function boundary creates no transaction, lock, or resource scope.
Files opened in the block still need explicit closing, shared-object mutation still needs synchronization, and remote calls still need timeout and retry rules.
"Scope" describes local name access here; it doesn't mean resource lifetime is automatically controlled.

Context-returning chains are especially good at hiding partial mutation.
If the first two assignments in `apply` succeed and the third throws, the object may already have changed even though the whole expression never returned normally.
For all-or-nothing configuration, validate first, construct an immutable value, or put the commit inside a type that can preserve its invariant.

A public API shouldn't make callers reverse-engineer an internal sequence of scope functions.
Repeated transformations deserve a named function, complex construction deserves validation in `build()`, and a flow with several failure reasons deserves an explicit result type.
Scope functions should support a clear structure, not substitute for one.

<!-- /deep -->

[Checkpoint: kotlin/scope-functions](https://codewiki.com/kotlin/scope-functions/#checkpoint)

## Further reading

- [Kotlin documentation: Scope functions](https://kotlinlang.org/docs/scope-functions.html)
- [Kotlin 2.4.10 standard-library source: `Standard.kt`](https://raw.githubusercontent.com/JetBrains/kotlin/v2.4.10/libraries/stdlib/src/kotlin/util/Standard.kt)
- [Kotlin documentation: Higher-order functions and lambdas](https://kotlinlang.org/docs/lambdas.html)
- [Kotlin documentation: Inline functions](https://kotlinlang.org/docs/inline-functions.html)
- [Kotlin language specification: Overload resolution](https://kotlinlang.org/spec/overload-resolution.html)
