As a newcomer to Kotlin, scope functions look like the Builder pattern on steroids.
There are 5 scope functions that we can combine to build values and objects with side effects that make sense and swallow those that don’t.
let: transform it
The idea is to take a value of type T, pass it through a lambda and assign into a val or var, or do something else.
For example:
val result = "hello".let {
it.length
}
In the this situation we will have result: Int = 5. We went from a String into an Int seamlessly.
It is true that we could achieve that result in many other ways without introducing a new keyword to our vocabulary, but imagine this other scenario:
val intermediateResult: String? = ...
val result = intermediateResult?.let {
it.length
} ?: 0
It becomes more fluid to simply continue the processing from intermediateResult directly.
Notice that inside the lamdbda the receiver is referenced as it.
In summary, let permits applying a transformation in an object and returns the result of that transformation.
run: compute using this
run is similar to let, but it becomes handy when performing an operation based on a receiver from which we intend to access many public properties.
Suppose we have these data classes:
data class Product(
val productId: String,
val quantity: Long,
val price: Double,
)
data class Order(
val orderId: String,
val products: List<Product>,
val createdAt: Instant,
)
We want to calculate the total price using this code below:
val order = repository.findOrder(id)
val totalPrice = order.run {
products.sumOf { it.price * it.quantity }
}
Notice that inside the lamdbda the receiver is referenced as this (which can be omitted).
The same code using let would be:
val order = repository.findOrder(id)
val totalPrice = order.let {
it.products.sumOf { it.price * it.quantity }
}
In summary, run permits applying a transformation in an object and returns the result of that transformation, similar to let but having this as the receiver instead of it.
also: side effect, keep it
It is usually a bad practice to have functions with side effects, but there are situations in which we really need them like for logging or collecting metrics.
also comes handy in situations like that:
val order = repository.findOrder(id)
.also {
logger.info("Found order: $it")
}
In the example above, order will contain the exact return from findOrder but we can rapidly perform a side effect and forget it.
Notice that inside the lamdbda the receiver is referenced as it.
In summary, also permits performing a side effect based on an object and returns the same object without altering it.
apply: configure/mutate this, keep this
Different than also, apply is intended to be used when we want to modify an object after its creation.
It is preferable to pass values to an object through its constructor, but if there is no other option, it is nice to keep all the boilerplate together.
Say order is a little bit different (var instead of val):
data class Order(
var orderId: String,
var products: List<Product>,
var createdAt: Instant,
)
And say OrderBuilder is a Java builder that we are importing.
val order = OrderBuilder().apply {
orderId = generateUniqueId()
products = listOf(product1, product2)
createdAt = Instant.now()
}
Which is preferable when compared to the alternative:
val order = OrderBuilder()
order.orderId = generateUniqueId()
order.products = listOf(product1, product2)
order.createdAt = Instant.now()
Notice that inside the lamdbda the receiver is referenced as this (which can be omitted).
In summary, apply permits changing public properties of an object after its creation and return this object (similar to also).
with: compute with this object as context
It is basically the same as run but with a different syntax.
Suppose we have those same data classes and want to calculate the total price of an order. Using with that would be:
val order = repository.findOrder(id)
val totalPrice = with(order) {
products.sumOf { it.price * it.quantity }
}
It is possibly a matter of taste, but depending on the situation, it might be handy to keep the notion of “object in context”.
Summary
Scope functions help making our code more expressive. It permits chaining operations in ways that enclose together code that should not leak during oblivious refactoring.
| Object available as | Returns | |
|---|---|---|
let |
it |
block result |
also |
it |
original object |
run |
this |
block result |
apply |
this |
original object |
with |
this |
block result |