The run
function in Kotlin is a versatile scope function that combines features of both let
and with
. It provides a way to execute a block of code within an object's context and return a result. In this guide, we'll explore how run
works, when to use it, and how it compares to other scope functions.
Understanding run
The run
function exists in two forms: as an extension function and as a standalone function. Both serve different purposes but share similar core functionality. Let's examine each variant:
Extension Function run
class Configuration {
var host: String = "localhost"
var port: Int = 8080
var secure: Boolean = false
}
val config = Configuration().run {
host = "api.example.com"
port = 443
secure = true
// Returns the result of buildUrl()
buildUrl()
}
Standalone Function run
val serviceUrl = run {
val host = System.getenv("SERVICE_HOST") ?: "localhost"
val port = System.getenv("SERVICE_PORT")?.toIntOrNull() ?: 8080
"http://$host:$port"
}
Key Characteristics of run
The run
function has several distinctive features:
- Context Object: Within the block, the object is accessed using
this
(can be omitted) - Return Value: Returns the last expression in the lambda
- Receiver: Can be used both as an extension and standalone function
Let's see these characteristics in action:
class UserProcessor {
private fun validateEmail(email: String): Boolean = email.contains("@")
private fun normalizeEmail(email: String): String = email.lowercase()
fun processUser(user: User?) {
user?.run {
// 'this' refers to User, can be omitted
if (!validateEmail(email)) {
throw IllegalArgumentException("Invalid email: $email")
}
// Modify user properties using 'this' context
email = normalizeEmail(email)
lastUpdated = LocalDateTime.now()
// Return processing result
true
} ?: throw IllegalArgumentException("User cannot be null")
}
}
Common Use Cases for run
1. Initializing Objects with Logic
The run
function excels when you need to initialize an object and perform complex logic:
class DatabaseConnection(
private val config: DatabaseConfig
) {
private val client = run {
// Complex initialization logic
val credentials = loadCredentials()
val poolConfig = createPoolConfig()
val retryPolicy = defineRetryPolicy()
DatabaseClient.builder()
.credentials(credentials)
.poolConfig(poolConfig)
.retryPolicy(retryPolicy)
.build()
}
private fun loadCredentials(): Credentials {
// Implementation
}
private fun createPoolConfig(): PoolConfig {
// Implementation
}
private fun defineRetryPolicy(): RetryPolicy {
// Implementation
}
}
2. Computing Values Based on Object State
When you need to compute a value using multiple properties of an object:
data class Rectangle(val width: Double, val height: Double) {
fun calculateProperties() = run {
val area = width * height
val perimeter = 2 * (width + height)
val diagonal = sqrt(width * width + height * height)
Properties(
area = area,
perimeter = perimeter,
diagonal = diagonal
)
}
}
3. Grouping Operations
The run
function helps group related operations while maintaining a clean scope:
class OrderProcessor {
fun processOrder(order: Order) {
val result = order.run {
validate()
calculateTax()
applyDiscounts()
computeFinalPrice()
}
sendConfirmation(result)
}
}
Best Practices
1. Use run for Computing Values
When you need to compute a value using multiple operations:
val message = run {
val greeting = if (LocalTime.now().hour < 12) "Good morning" else "Good afternoon"
val userName = user.fullName ?: "Guest"
"$greeting, $userName!"
}
2. Prefer run Over Nested let Calls
Instead of nesting multiple let
calls, use run
for cleaner code:
// Avoid this
user?.let { user ->
user.address?.let { address ->
address.city?.let { city ->
processCityData(city)
}
}
}
// Better approach using run
user?.run {
address?.run {
city?.let { processCityData(it) }
}
}
3. Use for Non-Null Assertions
The run
function works well with Elvis operator for null safety:
fun getUser(id: String): User {
return userCache[id]?.run {
// Work with non-null user
lastAccessed = LocalDateTime.now()
this
} ?: run {
// Create new user if not found
User(id = id).also { userCache[id] = it }
}
}
Comparing run with Other Scope Functions
run vs let
// run: Uses 'this', good for object operations
user.run {
name = name.capitalize()
email = email.lowercase()
this // Returns the user object
}
// let: Uses 'it', good for transformations
user.let { user ->
UserDto(
name = user.name,
email = user.email
)
}
run vs apply
// run: Returns lambda result
val length = user.run {
validateName()
name.length // Returns length
}
// apply: Returns the object
val user = User().apply {
name = "John"
email = "[email protected]"
} // Returns user
run vs with
// run: Extension function, null-safe
user?.run {
processName()
processEmail()
}
// with: Regular function, not null-safe
with(user) {
processName()
processEmail()
}
Common Pitfalls
1. Overusing run for Simple Operations
Don't use run
when simpler approaches work:
// Unnecessary use of run
val name = run { "John" }
// Better
val name = "John"
2. Confusing run with apply
Remember that run
returns the lambda result, not the object:
// Might be unexpected
val user = User().run {
name = "John"
email = "[email protected]"
true // Returns true, not the user object!
}
// If you need the object back, use apply instead
val user = User().apply {
name = "John"
email = "[email protected]"
} // Returns the user object
Conclusion
The run
function is a powerful tool in Kotlin's scope function family that combines the benefits of both let
and with
. It's particularly useful for computing values, grouping operations, and handling complex initialization logic. By understanding its characteristics and following best practices, you can write more concise and maintainable code.
Remember that run
is most valuable when you need to:
- Compute a value using object properties
- Group related operations
- Handle complex initialization logic
- Work with nullable objects in a fluent way
Choose run
when these are your requirements, and consider other scope functions when different characteristics would better serve your needs.