Choose a scope function by receiver and result
let and also expose the receiver as an argument, usually it. run, with, and apply expose it as this. apply and also return the original receiver; let, run, and with return the lambda's result.
data class Plan(var title: String = "", var minutes: Int = 0)
fun main() {
val plan = Plan().apply {
title = "Kotlin"
minutes = 25
}
val label = plan.run { "$title: $minutes min" }
println(label)
}
Here apply configures an object, and run computes a string from it. Neither function creates a coroutine or changes threads. with(plan) { ... } is useful when the receiver is already known; it is not a nullable-safe call by itself.
Nested scope functions make this and it ambiguous. Prefer named variables when there are several objects or side effects.
Exercise
Replace the example's run with let, naming its argument currentPlan. Confirm identical output. Then intentionally use also and inspect the inferred result type.
Check: also returns a Plan, even if the final expression in its lambda is a string.