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 asit, returns the block result. Typical:value?.let { use(it) }to run code only for non-null values, or transforming a value.run: object asthis, returns the block result. Computing something from an object's members.with(obj) { }: likerunbut not an extension; "with this object, do...".apply: object asthis, returns the object. Configuring a newly created object:Paint().apply { color = RED; width = 2f }.also: object asit, 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.