In Kotlin, the standard higher-order functions apply, also, let, and run were introduced to simplify chainable configuration of objects and local modifications with minimal boilerplate code. These functions facilitate work with mutable and immutable objects, allow for concise expression of transformation chains, and alleviate some issues with variable scope.
These functions were derived from the builder pattern and fluent interface approaches. Their introduction is attributed to the desire to make code cleaner and free from excessive declaration of auxiliary variables.
The traditional approach requires multiple accesses to the object or extracting temporary variables. This reduces readability and increases the risk of errors:
val user = User() user.name = "Alex" user.age = 26 user.isActive = true
Using the functions apply, also, let, and run increases expressiveness:
val user = User().apply { name = "Alex" age = 26 isActive = true }
Brief descriptions:
data class User(var name: String = "", var age: Int = 0, var isActive: Boolean = false) val configuredUser = User().apply { name = "Alice" age = 30 isActive = true } debugUser(configuredUser.also { println("User is configured: $it") }) val emailLength = configuredUser.email?.let { it.length } ?: 0 val description = configuredUser.run { "$name ($age)" }
Key features:
Can the let function modify the object it works with?
The let function is not meant to modify the object. It is more about applying a transformation to the value and returning the result. It’s important to remember that inside let, it refers to the value, not this.
val upperName = user.name.let { it.uppercase() } // let does not modify user
What is the difference between apply and run?
Both work with the context this inside the lambda, the difference lies in the returned value:
// apply val building = StringBuilder().apply { append("start-") append("end") } // building is a StringBuilder // run val result = StringBuilder().run { append("start-") append("end") toString() } // result is a String
Can apply and let be nested? If so, when is this justified?
Yes, nesting is gently recommended for aggregation or step-by-step configuration of objects, especially when working with nullable:
val userInfo = user?.apply { isActive = true }?.let { "${it.name} is active: ${it.isActive}" }
Let is used everywhere in the code for modifying the object, mixing apply and let — as a result, the object does not change where expected, and its value is lost in the chains.
Pros:
Cons:
Apply and also are used for configuration and logging, let only for working with nullable and obtaining results, run — for transformation chains needing a computed new value.
Pros:
Cons: