In Kotlin, run, apply, also, and with are scope functions. They help make code cleaner by giving you a temporary scope around an object.
The main differences are:
| Function | Object reference | Returns | Best used for |
|---|---|---|---|
apply |
this |
The original object | Configuring an object |
also |
it |
The original object | Side effects, logging, validation |
run |
this |
Lambda result | Computing a result from an object |
with |
this |
Lambda result | Grouping operations on an object |
apply: configure an object
Use apply when you want to initialize or configure an object and then keep using that same object.
val user = User().apply {
name = "Alice"
age = 30
active = true
}
This is cleaner than:
val user = User()
user.name = "Alice"
user.age = 30
user.active = true
apply returns the original object:
val button = Button().apply {
text = "Save"
isEnabled = true
}
Use apply for:
- object setup
- builder-style initialization
- setting multiple properties
also: perform side effects
Use also when you want to do something extra with an object without changing the main flow.
val user = repository.findUser(id)
.also {
logger.info("Loaded user: ${it.name}")
}
also uses it and returns the original object.
Another example:
val numbers = mutableListOf<Int>()
.also {
println("Created list")
}
Good use cases:
val file = File("data.txt")
.also {
require(it.exists()) { "File does not exist" }
}
Use also for:
- logging
- debugging
- validation
- side effects that should not change the returned value
run: compute a result from an object
Use run when you want to execute code using an object and return a computed result.
val description = user.run {
"$name is $age years old"
}
Here, run returns the last expression:
val length = "Kotlin".run {
lowercase().length
}
run is useful when you want to avoid repeating the object name:
val isAdult = user.run {
age >= 18
}
Use run for:
- computing a value
- using several properties/methods of an object
- keeping temporary logic contained
with: group operations on an object
with is similar to run, but it is not called as an extension function.
val result = with(user) {
"$name is $age years old"
}
This:
with(user) {
println(name)
println(age)
}
is cleaner than:
println(user.name)
println(user.age)
Use with when you already have an object and want to perform several operations with it.
val summary = with(order) {
"Order $id has $itemCount items and costs $total"
}
Quick comparison
apply
val person = Person().apply {
name = "Alice"
age = 30
}
Meaning:
Configure this object and return the object.
also
val person = Person("Alice")
.also {
println("Created person: $it")
}
Meaning:
Do something with this object and return the object.
run
val label = person.run {
"$name ($age)"
}
Meaning:
Use this object to compute and return a result.
with
val label = with(person) {
"$name ($age)"
}
Meaning:
With this object, compute and return a result.
Practical example
Without scope functions:
val request = Request()
request.url = "https://example.com"
request.method = "GET"
request.headers["Accept"] = "application/json"
logger.info("Created request: $request")
val summary = "Request to ${request.url} using ${request.method}"
With scope functions:
val request = Request().apply {
url = "https://example.com"
method = "GET"
headers["Accept"] = "application/json"
}.also {
logger.info("Created request: $it")
}
val summary = request.run {
"Request to $url using $method"
}
Rule of thumb
Use this simple guide:
Need to configure an object? apply
Need side effects like logging? also
Need to compute a result? run
Need to operate on an existing obj? with
Or more specifically:
- Use
applywhen the lambda modifies or configures the receiver. - Use
alsowhen the lambda performs side effects and should not affect the returned value. - Use
runwhen the lambda produces a new value. - Use
withwhen you want to group operations on an existing object.
One warning: avoid overusing them
Scope functions can make code cleaner, but too many nested scope functions can make code harder to read.
Avoid this:
user.apply {
address?.run {
city.also {
logger.info(it)
}
}
}
Prefer clearer code when nesting gets confusing:
val address = user.address ?: return
logger.info(address.city)
In short: scope functions are best when they reduce repetition and make intent obvious.
