Kotlin scope functions all do the same broad thing: they execute a block of code in the context of an object. The main differences are:
- How you refer to the object:
thisorit - What the function returns: the object itself or the block result
Quick decision table
| Function | Object reference | Returns | Best for |
|---|---|---|---|
let |
it |
Lambda result | Transforming a value, null-safe execution |
run |
this |
Lambda result | Computing a result from an object |
with |
this |
Lambda result | Grouping operations on an existing non-null object |
apply |
this |
The original object | Configuring or initializing an object |
also |
it |
The original object | Side effects like logging, validation, debugging |
Use let when you want to transform a value
let is useful when you want to take an object and produce another value.
val name = "kotlin"
val length = name.let {
it.uppercase().length
}
println(length) // 6
It is also very common with nullable values:
val email: String? = "[email protected]"
email?.let {
println("Sending email to $it")
}
Use let when you are thinking:
“If this value exists, do something with it or turn it into something else.”
Use apply when you want to configure an object
apply returns the original object, so it is ideal for initialization.
data class User(
var name: String = "",
var age: Int = 0,
var active: Boolean = false
)
val user = User().apply {
name = "Alice"
age = 30
active = true
}
Inside apply, the object is available as this, so you can access its properties directly.
Use apply when you are thinking:
“Create this object and set it up.”
Use also when you want side effects without changing the chain result
also returns the original object, like apply, but the object is referenced as it.
This makes it good for logging, debugging, validation, or extra actions.
val numbers = mutableListOf(1, 2, 3)
.also {
println("Original list: $it")
}
.apply {
add(4)
}
.also {
println("Updated list: $it")
}
println(numbers) // [1, 2, 3, 4]
Use also when you are thinking:
“Do this extra thing, but keep passing the same object along.”
Use run when you want to compute a result using an object
run uses this and returns the lambda result.
val message = StringBuilder().run {
append("Hello, ")
append("Kotlin")
toString()
}
println(message) // Hello, Kotlin
This is useful when you want to perform multiple operations and return a final computed value.
Use run when you are thinking:
“Use this object to calculate a result.”
Use with when you already have an object and want to group operations
with is not called as an extension in the same style as the others. You pass the object as an argument.
val builder = StringBuilder()
val result = with(builder) {
append("Hello, ")
append("Kotlin")
toString()
}
println(result) // Hello, Kotlin
Use with when you are thinking:
“With this existing object, do several things.”
with is usually best for non-null objects. For nullable values, prefer ?.let or ?.run.
The simplest way to choose
Ask two questions:
1. Do you want to return the original object?
If yes:
- Use
applyfor configuration - Use
alsofor side effects
val user = User().apply {
name = "Alice"
}
val user = getUser().also {
println("Loaded user: $it")
}
2. Do you want to return a new result?
If yes:
- Use
letwhen you preferit - Use
runwhen you preferthis - Use
withwhen the object already exists and is non-null
val length = name.let {
it.length
}
val text = builder.run {
append("Done")
toString()
}
Rule of thumb
let = transform or null-check
run = compute a result using this object
with = operate on an existing object
apply = configure an object
also = perform side effects
Common examples
Null-safe call
val username: String? = "sam"
username?.let {
println("Username length: ${it.length}")
}
Best choice: let
Object setup
val request = Request().apply {
method = "GET"
url = "/users"
}
Best choice: apply
Logging in a chain
val result = loadUsers()
.also { println("Loaded ${it.size} users") }
.filter { it.active }
Best choice: also
Build a value from several operations
val summary = users.run {
val activeCount = count { it.active }
"Active users: $activeCount"
}
Best choice: run
Group repeated calls on one object
val result = with(StringBuilder()) {
append("A")
append("B")
append("C")
toString()
}
Best choice: with
Avoid overusing scope functions
Scope functions are useful, but chaining too many can make code harder to read:
val result = user
.also { println(it) }
.let { transform(it) }
.also { save(it) }
.run { toDto() }
Sometimes plain code is clearer:
println(user)
val transformed = transform(user)
save(transformed)
val result = transformed.toDto()
A good guideline is: use scope functions when they make intent clearer, not just to make code shorter.
