Scope functions: let, apply, also, run, with

Kotlin · Practical · 10 min

Scope functions appear in nearly all Kotlin code; picking the right one keeps it readable.

The five scope functions run a block of code in the context of an object. They differ in two ways: how the block refers to the object (it or this), and what the call returns (the block's result or the object itself).

  • let: object as it, returns the block result. Typical: value?.let { use(it) } to run code only for non-null values, or transforming a value.
  • run: object as this, returns the block result. Computing something from an object's members.
  • with(obj) { }: like run but not an extension; "with this object, do...".
  • apply: object as this, returns the object. Configuring a newly created object: Paint().apply { color = RED; width = 2f }.
  • also: object as it, returns the object. Side effects in a chain: logging, validation, adding to a list.

Rules of thumb: apply for configuration, also for "and also do this", let for null checks and mapping, run/with for grouping calls on one object.

Don't nest them deeply or chain many; when it and this become ambiguous, a local variable is clearer. takeIf { cond } and takeUnless return the object or null and pair nicely with ?.let.

Example

data class Config(var host: String = "", var port: Int = 0, var debug: Boolean = false)

fun main() {
    val cfg = Config().apply {          // configure, returns the object
        host = "localhost"
        port = 8080
    }
    println(cfg)                        // Config(host=localhost, port=8080, debug=false)

    val url = cfg.run { "http://$host:$port" }       // compute from members
    println(url)                        // http://localhost:8080

    val log = mutableListOf<String>()
    val users = listOf("ada", "bo")
        .also { log += "loaded ${it.size}" }         // side effect, passes list on
        .map { it.replaceFirstChar(Char::uppercase) }
    println("$users $log")              // [Ada, Bo] [loaded 2]

    val input: String? = " 42 "
    val n = input?.trim()?.toIntOrNull()?.let { it * 2 } ?: 0
    println(n)                          // 84
    val port = with(cfg) { if (debug) 9000 else port }
    println(port)                       // 8080
    println(cfg.port.takeIf { it > 1024 })           // 8080
}

Remember

  • let/also use it; run/with/apply use this
  • apply and also return the object; let, run and with return the block result
  • apply to configure, also for side effects, let for null checks
  • Prefer a named variable over deeply nested scope functions

Watch out for

  • Using apply when you meant let, and getting the object instead of the computed value.

Check yourself

  1. What does "abc".also { it.uppercase() } return?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: kotlinlang.org/docs/scope-functions.html