Scope Functions

let, run, with, apply, also: five ways to execute a block against an object, each differing in receiver and return value.

let run with apply also Decision table
๐ŸŽฏ

What Scope Functions Are

A scope function takes an object, runs a lambda in some context related to it, and returns something โ€” either the object itself or the lambda's result. All five are just ordinary inline extension functions in the standard library, not language keywords; you could write your own equivalent. They exist purely for readability: chaining several operations on one object without repeating its name at every step, or scoping a temporary computation without polluting the surrounding scope with an extra named variable.

              Refers to the object as:     Returns:

  let         it (lambda argument)          lambda result
  run         this (lambda receiver)        lambda result
  with        this (lambda receiver)        lambda result
  apply       this (lambda receiver)        the object itself
  also        it (lambda argument)          the object itself

  Two axes to remember:
   - argument (it) vs receiver (this) โ€” how you refer to the object inside
   - returns lambda result vs returns the object โ€” what the call evaluates to
๐Ÿ’ก
The five names don't need memorizing in isolation โ€” the diagram above is the whole system. Once you know an operation is either it-based or this-based, and either "returns a new value" or "returns the same object," there are exactly five combinations, and four of the five are actually used (there's no this-based, argument-taking, object-returning combination โ€” that gap is filled by also plus a receiver-style call being redundant with apply).
๐Ÿ”ค

let

let refers to the object as it and returns whatever the lambda's last expression evaluates to. It's most at home paired with the null-safety operator ?. โ€” running a block only when a nullable value isn't null โ€” and for transforming a value into something else inline, without a separate named intermediate variable.

Null-safe execution and inline transformation let
val name: String? = getNameFromCache()

// Runs only if name isn't null; `it` refers to the non-null String
name?.let {
    println("Welcome, $it")
}

// let's return value is the lambda's last expression โ€”
// useful for transforming inline, without a named temp variable
val length: Int? = name?.let { it.trim().length }

// Also common: scoping a value to just the lines that need it
val greeting = computeUserName().let { userName ->
    "Hello, $userName!"
}
๐Ÿƒ

run

run is let's receiver-style twin: same return-the-lambda-result behavior, but the object is referred to as this (implicit, usually), so its own members are called without a qualifier. There's also a non-extension form, run { } with no receiver at all, useful purely as a scoped block that evaluates to a value.

Object configuration + computed result in one block run
class Connection {
    var host: String = ""
    var port: Int = 0
    fun isValid() = host.isNotBlank() && port > 0
}

// this is implicit: host/port/isValid() called directly, no qualifier
val connectionIsReady = Connection().run {
    host = "localhost"
    port = 5432
    isValid()   // last expression: run's return value
}
println(connectionIsReady)  // true

// No-receiver run: just a scoped block that evaluates to a value
val config = run {
    val env = System.getenv("ENV") ?: "dev"
    "config-$env.json"
}
๐Ÿค

with

with is functionally identical to run โ€” receiver as this, returns the lambda's result โ€” but it's called differently: with(obj) { ... } instead of obj.run { ... }, because with is a plain top-level function taking the object as a parameter, not an extension function on it. It reads best when the object already exists as a variable and you're not chaining off a call that just produced it.

Grouping several calls on an existing object with
val sb = StringBuilder()

// with(sb) reads naturally: "with sb, do the following"
val result = with(sb) {
    append("Hello")
    append(", ")
    append("World")
    toString()   // last expression: with's return value
}
println(result)  // "Hello, World"
โ„น๏ธ
Because with isn't an extension function, it doesn't chain off a safe call (?.with isn't valid syntax) and can't be used at the end of a fluent call chain. That's the main practical reason to reach for run instead when the receiver is the result of a preceding expression, like ?.run { ... } for a nullable value.
๐Ÿ› 

apply

apply refers to the object as this, same as run, but always returns the object itself regardless of the lambda's last expression. This makes it the natural fit for object configuration: build or mutate something, then get the same object back to continue using โ€” the classic replacement for a fluent builder pattern.

Configure-then-return-the-object apply
class TextView {
    var text: String = ""
    var textSize: Int = 12
    var color: String = "black"
}

// apply returns the TextView itself, configured โ€” assignable directly
val label = TextView().apply {
    text = "Hello"
    textSize = 18
    color = "blue"
}
// label is a TextView, fully configured in one expression

// Common at construction time, avoiding a separate configure step
val list = mutableListOf(1, 2, 3).apply {
    add(4)
    removeAt(0)
}
โž•

also

also is apply's it-based twin: still returns the object unchanged, but refers to it as it rather than an implicit receiver. That makes it the right choice when the block is a genuine side effect โ€” logging, validation, a debug print โ€” where writing the object's name explicitly (via it) makes the side-effecting call clearer than an implicit this would.

Side effects in the middle of a chain also
val numbers = mutableListOf(1, 2, 3)
    .also { println("Initial: $it") }   // logs, then passes the list along unchanged
    .apply { add(4) }
    .also { println("After add: $it") }

// Validation as a side effect, still returning the original value
fun createUser(name: String): User = User(name).also {
    require(it.name.isNotBlank()) { "name can't be blank" }
}
๐Ÿ“‹

Decision Table

Situation Function Why
Null-check + act on a nullable valueletPairs with ?.; it is non-null smart-cast inside
Transform a value inline, no temp variableletReturns the lambda's result
Configure a newly-created object, keep itapplyReturns the object itself; this-based for terse member access
Run several calls on an existing variablewithReads as "with X, do Y"; not chainable off ?.
Initialize + compute a derived result togetherrunthis-based access, but returns the computed result, not the object
Side effect (log, validate) mid-chainalsoit makes the side-effecting call's subject explicit
โš–๏ธ

Idiomatic Uses & Abuses

โš ๏ธ
Scope functions compose, but nesting them deeply is a fast way to make code unreadable โ€” several stacked its from different scope functions shadow each other exactly like nested lambdas do (see Gotchas). If a block needs more than one level of scope-function nesting, naming the parameter explicitly at each level (or breaking the chain into named intermediate variables) almost always reads better than leaning further into implicit it/this.
When a plain variable beats a scope function Restraint
// Overused: let adds nothing here except an extra indirection layer
val x = computeValue().let { it + 1 }

// Clearer: just a plain expression, no scope function needed at all
val x2 = computeValue() + 1

// Justified: genuinely null-safe transformation, let earns its place
val length = maybeString?.let { it.trim().length } ?: 0
๐Ÿ’ก
Reach for a scope function when it removes real repetition (the object's name, over and over) or expresses real intent (null-safety, configuration, a deliberate side effect). If a scope function's only job is "wrap this one expression," it's usually adding a layer of indirection without adding any actual clarity.
๐Ÿ“‹

Quick Reference

Function Object reference Returns
letitLambda result
run (extension)thisLambda result
run (no receiver)n/aLambda result โ€” just a scoped block
with(obj) { }thisLambda result โ€” not an extension function
applythisThe object itself
alsoitThe object itself