Kotlin code is full of short blocks like user?.let { ... } and Paint().apply { ... }. These are scope functions: five small standard-library functions, let, run, with, apply and also, that run a block of code with an object as its context.
They don't let you do anything you couldn't do with ordinary variables. Their value is readability: configuring an object in one tidy block, running code only when a value isn't null, or adding a logging step without breaking a chain. Used well, they make intent obvious. Used everywhere, they make code harder to follow, so knowing which one fits is the real skill.
The five differ in just two ways: how the block refers to the object (it or this) and what the call returns (the object itself, or the block's result). This lesson shows each one in the situation it suits best, and gives a simple guide for choosing.
The two differences
| Function | Object inside the block | Returns |
|---|---|---|
let | it | The block's result |
run | this | The block's result |
with(obj) | this | The block's result |
apply | this | The object itself |
also | it | The object itself |
With this, you can call the object's members directly: name instead of it.name. With it, the object is clearly named, which reads better when you pass it to other functions. Returning the object suits configuration and chaining; returning the result suits calculating something new.
let and apply
let is most often used with a safe call, to run code only when a value isn't null:
user.email?.let { sendWelcome(it) }
val length = name?.let { it.trim().length } ?: 0apply configures an object and returns it, which is ideal right after creating something:
val request = Request().apply {
url = "https://api.example.com/users"
method = "POST"
timeoutSeconds = 10
}Inside apply, properties are set directly, without repeating request. on every line.
also, run and with
also does something extra with an object, such as logging or validating, and passes the object on unchanged:
val user = createUser(form).also { println("Created ${it.id}") }run and with compute a result from an object's members. run is called on the object (and works with ?.); with takes the object as an argument:
val summary = order.run { "$id: $quantity x $item" }
val area = with(rectangle) { width * height }takeIf { condition } returns the object if the condition is true, or null if not, which combines well with Elvis: val age = input.toIntOrNull()?.takeIf { it in 0..130 } ?: 0.
Choosing quickly
- Null check, then use the value:
?.let. - Set up a new object's properties:
apply. - Add a side effect (log, validate) in a chain:
also. - Calculate a value from an object's members:
runorwith.
If none of these fits naturally, an ordinary local variable is usually clearer. Avoid nesting scope functions inside each other: two levels of it or this quickly become confusing.
When this means something else: name clashes inside apply
Inside apply, run and with, a bare name is looked up in the nearest scope first. Local variables and function parameters come before the receiver's properties, which catches people out when they share a name:
class Person { var name = ""; var age = 0 }
fun makePerson(name: String, age: Int) = Person().apply {
this.name = name // the property gets the parameter
this.age = age
}Without this., the line name = name refers to the parameter on both sides, and since parameters are read-only, the compiler stops you with "'val' cannot be reassigned". When the outer variable is a var, there's no error at all: the assignment quietly changes the local variable and the object keeps its old value. Writing this. makes it clear which name you mean. If a block needs this. on most lines, also with a named parameter may read better.
The same naming trick helps with let. Instead of it, give the value a name that says what it is:
email?.let { address -> println("Sending to $address") }run on its own and takeUnless
run can also be called without any object. Then it's simply a block that produces a value, which is handy for keeping temporary variables out of the surrounding code:
val total = run {
val base = 40.0
val tax = base * 0.25
base + tax
} // 50.0; base and tax don't exist out heretakeUnless is the mirror image of takeIf: it keeps the value when the condition is false. nick.takeUnless { it.isBlank() } ?: "Anonymous" replaces a blank nickname with a default in one line.
Kotlin Scope Functions Syntax
Scope functions
x?.let { use(it) }
val obj = Thing().apply { prop = 1; other = "a" }
val y = obj.also { log(it) }
val r = obj.run { prop * 2 }
val s = with(obj) { "$prop and $other" }
val valid = n.takeIf { it > 0 } ?: 1Kotlin Scope Functions Summary
- Scope functions run a block with an object as context: let, run, with, apply and also.
- let and also refer to the object as it; run, with and apply use this.
- apply and also return the object; let, run and with return the block's result.
- Typical uses: ?.let for null checks, apply for configuration, also for side effects, run/with for computing.
- takeIf returns the object or null; keep scope functions shallow and readable.
Kotlin Scope Functions Examples
Each example shows the code, the output it produces and what each part does.
Configuring with apply and logging with also
class Email {
var to = ""
var subject = ""
var body = ""
override fun toString() = "To: $to | $subject | ${body.length} chars"
}
fun main() {
val email = Email().apply {
to = "sam@example.com"
subject = "Your order has shipped"
body = "Hi Sam, your order #1042 is on its way."
}.also { println("Prepared: $it") }
println(email.subject)
}Prepared: To: sam@example.com | Your order has shipped | 39 chars
Your order has shippedLine-by-line explanation
- Lines 9 to 13apply sets properties directly through this and returns the Email.
- Line 13also receives the finished Email as it and passes it on unchanged.
- Line 15email holds the configured object, because both apply and also return it.
let for nullable values, and takeIf
fun main() {
val inputs = listOf("29", "abc", "150", " 41 ")
for (text in inputs) {
val age = text.trim().toIntOrNull()?.takeIf { it in 0..130 }
age?.let { println("Valid age: $it, next year ${it + 1}") }
?: println("Rejected: '$text'")
}
val nickname: String? = null
val shown = nickname?.let { "@$it" } ?: "(no nickname)"
println(shown)
}Valid age: 29, next year 30
Rejected: 'abc'
Rejected: '150'
Valid age: 41, next year 42
(no nickname)Line-by-line explanation
- Line 4toIntOrNull gives null for bad text, and takeIf turns out-of-range ages into null too.
- Lines 5 to 6let runs only for a real age; Elvis handles the null case.
- Line 10let transforms the value when present; Elvis supplies a default otherwise.
run and with to compute results
data class Order(val id: Int, val item: String, val qty: Int, val unitPrice: Double)
fun main() {
val order = Order(1042, "Notebook", 3, 3.50)
val total = order.run { qty * unitPrice }
val line = with(order) { "#$id: $qty x $item = £${"%.2f".format(qty * unitPrice)}" }
println(total)
println(line)
}10.5
#1042: 3 x Notebook = £10.50Line-by-line explanation
- Line 6run uses this, so qty and unitPrice are read directly, and returns the calculation.
- Line 7with does the same but takes the object as an argument; it reads well for building text.
Common Kotlin Scope Functions Mistakes
Using apply when you want a result
val length = name.apply { trim().length } // length is the String, not a numberval length = name.let { it.trim().length }apply always returns the object, ignoring the block's result.
let without a safe call
email.let { send(it) } // error if email is String?: it is still nullableemail?.let { send(it) }Nesting scope functions
user?.let { order?.let { it.items.also { println(it) } } } // which it is which?if (user != null && order != null) {
println(order.items)
}Kotlin Scope Functions in Real Projects
Scope functions in practice
Android code uses apply to set up views, intents and notification builders, ?.let to handle optional data from screens and APIs, and also to log or record analytics in the middle of a chain. Many teams agree on simple conventions for which scope function to use where, so the codebase reads consistently.
Kotlin Scope Functions FAQ
What is the difference between let and also in Kotlin?
Both give you the object as it, but they return different things. let returns the result of the block, so it's for turning a value into something else. also returns the original object, so it's for side effects, such as logging, in the middle of a chain.
What is the difference between apply and run?
Both give you the object as this. apply returns the object itself, which makes it ideal for setting up properties on something you've just created. run returns the block's last expression, which suits computing a value from the object's members.
What is the difference between run and with?
They do the same job, computing a result with the object as this, but run is called on the object while with takes it as an argument. That means only run works with a safe call, as in user?.run { "$name ($age)" }. Use with for a non-null object when you want to group several calls on it.
Are Kotlin scope functions slow?
No. let, run, with, apply and also are inline functions, so the compiler copies the block's code straight into the place you call it. There's no extra object for the lambda and no function call left at run time.
What does takeIf do in Kotlin?
value.takeIf { condition } returns the value if the condition is true and null if it isn't. It pairs well with Elvis for defaults, such as input.toIntOrNull()?.takeIf { it > 0 } ?: 1, and with ?.let to run code only for values that pass a check.
Kotlin Scope Functions Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Complete the code so the block runs only when name isn't null.
name?. { println("Hi, $it") }
Which scope function returns the object itself and refers to it as this?
What does this print?
val n = 5.let { it * 2 }.also { println("got $it") }
println(n)
What does this print?
val x = "abc".takeIf { it.length > 5 } ?: "short"
println(x)
Which is the best choice for logging a value in the middle of a chain without changing it?
Kotlin Scope Functions Quiz
5 questions to check what you learned.
Which scope functions use
itto refer to the object?- apply and run
- let and also
- with and run
- All of them
What does
letreturn?- The original object
- Unit
- The result of the block
- A Boolean
How is
withdifferent fromrun?- with returns the object
- with takes the object as an argument instead of being called on it
- with uses it
- There's no difference at all
What does
takeIf { it > 0 }return when the value is -3?- null
- -3
- 0
- false
When is a plain local variable better than a scope function?
- Never
- When the scope function makes it unclear what it or this refers to, such as in nested blocks
- Always
- Only in main