The five scope functions run a lambda with an object in scope. They differ only in whether the object is it or this inside the lambda, and whether the call returns the object or the lambda's result.
| Function | Object as | Returns | Typical use |
|---|---|---|---|
| let | it | lambda result | ?.let on a nullable |
| run | this | lambda result | configure, then compute |
| with(x) | this | lambda result | calls on one object |
| apply | this | the object | configure an object |
| also | it | the object | side effects, logging |
class CartLine { var bookId = 0; var qty = 1 }
fun main() {
println(books.find { it.id == 5 }?.let { "${it.title}: $${it.price}" } ?: "not found")
val line = CartLine().apply { bookId = 2; qty = 3 } // returns the object
println("subtotal " + with(books.first { it.id == line.bookId }) { price * line.qty })
println(line.run { "cart line: book #$bookId x $qty" }) // returns the lambda result
val evenIds = books.map { it.id }
.also { println("all ids $it") } // side effect, passes on
.filter { it % 2 == 0 }
println("even ids $evenIds, top rated ${books[2].rating.takeIf { it >= 4.5 }}")
}Output
The Clockmaker's Paradox: $16.2 subtotal 118.5 cart line: book #2 x 3 all ids [1, 2, 3, 4, 5, 6] even ids [2, 4, 6], top rated 4.8
takeIf returns the value when the predicate holds and null otherwise, so it pairs with ?.let. All of them are inline, so they cost no lambda allocation. The pitfall is nesting: inside an apply within a run, this is the innermost receiver. Use one scope function per expression, and name the parameter (let { book -> }) when nesting is unavoidable.