---
title: 'Scope Functions'
source: 'https://academia.sh/en/courses/kotlin/scope-functions'
course: 'The Kotlin Language'
language: en
updated: '2026-10-21T05:02:40+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Distinguishes `let`, `run`, `with`, `apply`, and `also` by receiver and return value'
    - 'Reads which object an expression works on (`this` or `it`)'
    - 'Measures that the object a scope function returns does not change its equality criterion'
---

# Scope Functions

Introduces the scope functions `let`, `run`, `with`, `apply`, and `also`; measures whether the receiver is `this` or `it` and what each returns, and shows that the result does not change the equality criterion.

In the previous lesson we carried lambdas as values. Scope functions are five ready-made higher-order functions that run these lambdas in the context of an object: `let`, `run`, `with`, `apply`, and `also`. The difference between them comes from two questions: how is the object reached inside the lambda, and what does the function return? At the end of the lesson a third question arrives: does the equality criterion of an object change when it passes through a scope function?

## Receiver and Return Value

A scope function gives the object it is called on to the lambda in one of two ways. `let` and `also` give the object as the lambda parameter `it`. `run`, `with`, and `apply` make the object the receiver: inside the lambda, the members of the object are reached with `this`, and most often with no prefix at all. This distinction is the first place to look when reading which object an expression works on.

The return value has two forms. `let`, `run`, and `with` return the result of the last expression of the lambda. `apply` and `also` return the object itself. That is, `apply` is used while building an object, and `also` is used to do a side job without touching the object.

In this lesson too, the measurements are made with the helper file introduced in the first lesson. The `Criterion` class takes a value, its twin, and a different value, and asks three questions; the file is given below exactly as it was.

```kotlin
// Criterion.kt — classifies the criterion that makes two values equal
class Criterion(val label: String, first: Any?, twin: Any?, other: Any?) {
    val sameObject = first === twin
    val equalTwin = first == twin
    val equalOther = first == other

    val name: String = when {
        sameObject -> "singleton"
        !equalTwin -> "identity"
        !equalOther -> "content"
        else -> "partial-content"
    }

    fun report() {
        println("$label: same-object=$sameObject, twin=$equalTwin, other=$equalOther -> $name")
    }
}

fun summarize(checks: List<Criterion>) {
    for (name in listOf("singleton", "identity", "content", "partial-content")) {
        println("$name: ${checks.count { it.name == name }}")
    }
}
```

This choice is a readability decision. If you want to use the members of the object inside the lambda without a prefix, one of the three functions with the receiver form is suitable; if you will pass the object under a name as an argument to something else, the two with the `it` form are suitable. The return value decides what you are left with after the block: the object itself, or a computed result.

The program below runs the five functions in turn. `Point` is a data class with two mutable fields.

```kotlin
// Main.kt — let, run, with, apply, and also
data class Point(var x: Int, var y: Int)

fun main() {
    val letResult = Point(1, 2).let { it.x + it.y }
    val runResult = Point(3, 4).run { x * y }
    val withResult = with(Point(5, 6)) { "$x,$y" }
    val applied = Point(0, 0).apply {
        x = 7
        y = 8
    }
    val alsoResult = Point(9, 9).also { println("observed: ${it.x}") }
    println(letResult)
    println(runResult)
    println(withResult)
    println(applied)
    println(alsoResult)
}
```

```
observed: 9
3
12
5,6
Point(x=7, y=8)
Point(x=9, y=9)
```

The first line came from the `println` call inside `also` and wrote `observed: 9`; the `also` block observed the object, and the object was returned as it was. In the `let` block the two fields were added through `it`, and `3` came out. In the `run` block `x * y` was written with the receiver, without a prefix, and `12` came out. `with` used the same receiver syntax and gave the text `"5,6"`, which is the result of the lambda. In the `apply` block the fields were assigned, and the function returned the object itself: `Point(x=7, y=8)`. The last line shows the object returned by `also`: `Point(x=9, y=9)`.

**LB52.** The five functions were measured only on `Point` and only with integer fields; uses of `?.let`, `?.apply`, and `takeIf` on a receiver that can be null are measured later in this lesson, only with `String?`.

**LB53.** `with` is not an extension but an ordinary function that takes the object as an argument; this distinction was only stated here, not measured.

## A Scope Function Does Not Change the Criterion

The measurement asks this: does passing an object through a scope function change whether its twin is counted as equal? Here `Point` is a data class with fields that do not change. We write three observations. `let-copy`: `Point(1, 2).let { it.copy() }`. `run-copy`: `Point(1, 2).run { copy() }`; the same copy, this time with the receiver syntax. `also-twin`: the same `source` variable is passed through `also { }` and compared with itself.

Since `let` and `run` return the result of the lambda, what is returned is a separate object built with `copy()`. `also`, on the other hand, returns its own object.

```kotlin
// Main.kt — what a scope function returns
data class Point(val x: Int, val y: Int)

fun main() {
    val source = Point(1, 2)
    val checks = listOf(
        Criterion("let-copy", Point(1, 2), Point(1, 2).let { it.copy() }, Point(1, 3)),
        Criterion("run-copy", Point(1, 2), Point(1, 2).run { copy() }, Point(1, 3)),
        Criterion("also-twin", source, source.also { }, Point(1, 3)),
    )
    for (check in checks) {
        check.report()
    }
    summarize(checks)
}
```

```
let-copy: same-object=false, twin=true, other=false -> content
run-copy: same-object=false, twin=true, other=false -> content
also-twin: same-object=true, twin=true, other=false -> singleton
singleton: 1
identity: 0
content: 2
partial-content: 0
```

`let-copy` and `run-copy` are content: `same-object=false`, `twin=true`. The copy that comes out of the scope function is not the same object as the source, but since it carries the same values, the `equals` of the data class counts them as equal. Which scope function was used did not determine the criterion; what determines the criterion is that `Point` is a data class.

`also-twin` is a singleton: `same-object=true`. Since `also` returns the object itself, the twin and the first value came out as the same object. The summary lines give one singleton, zero identity, two content, and zero partial content.

**LB54.** In the `also-twin` observation no twin was built; since the scope function returns the object itself, the comparison was made between an object and itself. This observation breaks the measurement condition, as explained below.

**LB55.** The `copy()` call produces a shallow copy; how the copy behaves in a data class that carries a nested object was not measured in this lesson.

## What the Five Functions Return

The real difference between scope functions comes down to two things: is the object reached with `it` or with `this`, and what does the expression return? The block below runs all five on the same `Point` object and tests the returned object against the source with `===`.

```kotlin
// Main.kt — what the scope functions return
data class Point(var x: Int, var y: Int)

fun main() {
    val p = Point(1, 2)
    println(p.let { it } === p)
    println(p.run { this } === p)
    println(with(p) { this } === p)
    println(p.apply { x = 10 } === p)
    println(p.also { it.y = 20 } === p)
    println(p.let { 5 })
    println(p.apply { x = 11 }.run { x + y })
    println(p)
}
```

```
true
true
true
true
true
5
31
Point(x=11, y=20)
```

All five lines gave `true`, but not for the same reason. `p.let { it } === p` and `p.run { this } === p` are true because the last expression of the lambda is the object itself. `with(p) { this } === p` is true for the same reason. `apply` and `also`, on the other hand, do not look at the value of the lambda; they always return the receiver object.

The sixth line gave `5`: the value of the expression `let { 5 }` is the last expression of the lambda, not the object. The seventh line is `31`: `apply { x = 11 }` returned the object, and `run { x + y }` computed the sum, the sum of 11 and 20, which is 31. The last line gave `Point(x=11, y=20)`, that is, the assignments in the previous five calls changed the object itself. These assignments were not made on a local copy but on `p`.

The rule: `let`, `run`, and `with` give the result of the lambda; `apply` and `also` give the object. The frequent mistake is to write a value on the last line inside `apply` and assume that it will be returned. Java has no equivalent construct; for chaining, the methods have to return `this` (the builder pattern).

**LB56.** The return values were measured with `Point` and integer results; `with` is not an extension but an ordinary function, and this distinction was not measured.

## Scope Functions with null, and takeIf

The most frequent use is to process a value that may be null safely with `?.let`. The block below tries this with a filled and an empty `String?`, looks at the difference between `?.let` and `.let { it?.... }`, and tests the functions `takeIf` and `takeUnless`.

```kotlin
// Main.kt — scope functions with null, and takeIf
fun main() {
    val present: String? = "kotlin"
    val missing: String? = null
    println(present?.let { it.length })
    println(missing?.let { it.length })
    println(missing.let { it?.length })
    println(missing?.apply { length })
    println(missing?.let { it.length } ?: -1)
    println(present.takeIf { it?.startsWith("k") == true })
    println(present.takeUnless { it?.startsWith("k") == true })
    println("abc".takeIf { it.length > 5 })
}
```

```
6
null
null
null
-1
kotlin
null
null
```

`present?.let { it.length }` gave `6`; `missing?.let { it.length }` gave `null`. The safe call `?.` did not run the lambda at all when the receiver was null. The third line also gave `null`, but the reason is different: in the expression `missing.let { it?.length }`, `let` is also called on the null value, the lambda runs, and `it?.length` inside produces a null result. Even if the results of these two forms are the same, the lambda did not run in the first and did run in the second. The fourth line is `null`: `missing?.apply { length }` also ran nothing because the receiver was null, and the result is the null value. The fifth line is `-1`: the elvis operator turned the null result into a default.

`takeIf` returns the object if the condition is true and `null` if it is false. `present.takeIf { ... }` gave `kotlin`, because the string starts with `k`. `takeUnless` works in reverse and gave `null` with the same condition. `"abc".takeIf { it.length > 5 }` gave `null`, because the condition was not met. The frequent mistake is to use the result of `takeIf` directly, forgetting that it is of a nullable type; the compiler reports a compile error in this case, but this error was not measured.

**LB57.** The null measurement was made with `String?`; side effects in the `takeIf` condition and chains of `takeIf` were not measured in this lesson.

## Nested Scope Functions and this

When two scope functions are written nested, which object do `this` and `it` look at? The block below runs a second `run` inside a `run`, reaches the outer receiver with a label, then uses a `run` without an object, and reads `size` in nested `apply` calls.

```kotlin
// Main.kt — nested scope functions and this
data class Box(var size: Int)

fun main() {
    val box = Box(1)
    val result = box.run outer@{
        size = 5
        Box(size * 2).run { "inner: $size, outer: ${this@outer.size}" }
    }
    println(result)
    val note = run {
        val temp = 21
        temp * 2
    }
    println(note)
    Box(3).apply {
        Box(4).apply {
            println(size)
        }
        println(size)
    }
}
```

```
inner: 10, outer: 5
42
4
3
```

The first line gave `inner: 10, outer: 5`. In the outer `run`, `size = 5` was assigned, and in the inner `run` a new `Box(10)` was built with `size * 2`. In the inner block, `size` looks at the nearest receiver, that is, the `Box(10)` object. To reach the outer receiver, `this@outer.size` is written, and `5` is read. For this reason, nested receiver use produces code that is hard to read; giving the name `it` with `also` and `let` is clearer. The frequent mistake is to write `size` in the inner block and assume that the field of the outer object is being read.

The second line gave `42`: a `run { }` without an object only opens a block and returns the last expression. The third and fourth lines gave `4` and `3`: inside the inner `apply`, `size` is the value 4 of the inner object, and inside the outer `apply` it is the value 3 of the outer object. When the inner block ends, the scope returns to the outer one. In Java, the same effect in inner classes comes from the syntax `Outer.this.field`; in Kotlin, the label is its counterpart.

**LB58.** The nested receiver measurement was made with two levels; the resolution of an unlabeled `this` in use nested three or more levels deep was not measured in this lesson.

## Habits That Come from the Measurements

Two questions are asked when choosing a scope function: is the object reached with `it` or `this`, and will the result be the object or the value of the lambda? The measurement confirmed the table: `apply` and `also` return the object, and `let`, `run`, and `with` return the value of the lambda. In practice, `apply` is used while building an object, `also` when adding a side effect (such as writing a log), `let` when transforming a value that may be null, and `run` and `with` when doing a calculation on an object and taking the result. This pairing is not a rule but a common habit; the same job can often be done with another function as well.

Overuse is a real risk. Three nested scope functions make it hard to follow which object the names `this` and `it` go to. In the measurement, `size` inside the inner `run` went to the inner object, and access to the outer value was made with `this@outer.size`. In use that goes beyond two nested levels, named local variables are clearer. As a rule, if one scope function is written inside another, either the outer one should be named with `it` or the code should be moved to a separate function.

With null values, `?.let` is the common pattern, and the measurement showed the difference between the two forms: `missing?.let { ... }` does not run the lambda at all, while `missing.let { it?.... }` runs the lambda with the null value. Even if the results look the same, the difference appears in a lambda that carries a side effect. `takeIf` and `takeUnless` put a condition into the chain: if the condition is not met, `null` returns and the following `?.` steps are skipped.

Another frequent mistake is to write a value on the last line of an `apply` block and assume that it will be returned. `apply` always returns the object, so the expression at the end is ignored. If you want to turn the result into another value, `let` or `run` should be chosen. In Java, the counterpart of these jobs is the builder pattern, which depends on every method returning `this`; Kotlin provides this at the language level, without changing the existing class.

Finally, `with` is not an extension but an ordinary function that takes the object as an argument. For this reason it cannot be attached with `?.` to an object that may be null; for a nullable receiver, `?.run` or `?.let` should be chosen.

## The Bounding Measurement

The `also-twin` observation breaks the measurement condition: `Criterion` expects a separately built twin of a value, whereas `also` gives the same object back. The singleton result that came out is not a finding about the criterion but a reflection of the return value rule of the function. For this reason, to show that a scope function does not change the criterion, the `let` and `run` observations are meaningful, while the `also` observation shows only the limit. If you see a scope function in an expression, ask these two questions: which object is this expression working on, and does the function return this object or the result of the lambda? The answer to the equality question always stays in the own type of the object.

## Summary

- `let` and `also` give the object as `it`; `run`, `with`, and `apply` give it as the receiver (`this`).
- `let`, `run`, and `with` return the result of the lambda; `apply` and `also` return the object itself.
- `Point(1, 2).let { it.copy() }` and `run { copy() }` gave separate but equal objects: content.
- Since `also` gives the same object back, `also-twin` came out as a singleton and broke the measurement condition.
- This lesson's criterion: **content**; bounding measurement: the `also` twin is a singleton and breaks the measurement condition because it builds no twin.

## Next Step

The language basics close here. In the next topic, `null` enters the equality question: when a value can be absent, is `null` the same as `null`, and how does the compiler follow this case inside the type? The topic starts with the lesson Null Safety.
