Scope Functions in Kotlin (let, apply, with, also, run)

Gaurav Rajput
4 min readAug 29, 2024

--

Sometimes while writing code we want to execute a few lines of code referencing an object. For that, we create a temporary scope in which we can access that object without the object name. These functions do not give any technical capabilities but enhance readability and conciseness.

All these functions do the same execute a block of code on an object .

Let's see how they differ from each other.

Let

Used to add a null check on objects.

val name: String? = "Kotlin" 
val result = name?.let {
println("The name is $it") // 'it' refers to 'name'
it.length // Returns the length of the string
}
println(result) // Output: 6

If let's say name is null. Then

val name: String? = null
val result = name?.let {
println("The name is $it") // This won't be printed
it.length
}
println(result) // Output: null

With

In simple words, you can use with it is when you want to perform several operations on an object and potentially return a result.In the code, with can be read as “With this object, do the following.

Let's understand this by example: —

class Person {
var name: String = ""
var age: Int = 0
var job: String = ""
var city: String = ""
}
fun main() {
val person = Person()
// Setting properties without 'with'
person.name = "Alice"
person.age = 30
person.job = "Engineer"
person.city = "New York"
// Printing the details
println("Name: ${person.name}, Age: ${person.age}, Job: ${person.job}, City: ${person.city}")
}

Here you can see we are repeatedly referencing the object (person in this case) for each operation. Is there any better way to write this code?
Let’s see -

fun main() {
val person = Person()

// Using 'with' to set properties
with(person) {
name = "Alice"
age = 30
job = "Engineer"
city = "New York"
}

// Printing the details
println("Name: ${person.name}, Age: ${person.age}, Job: ${person.job}, City: ${person.city}")
}

You can see now it's more concise and readable.

Lets see one more example when you are using with and returning value from with block

val person = Person()

// Using 'with' to set properties and return a summary string
val summary = with(person) {
name = "Alice"
age = 30
job = "Engineer"
city = "New York"
// Return a summary string
"Name: $name, Age: $age, Job: $job, City: $city"
}

// Printing the summary
println(summary)

Simply you can write whatever you want to return as a last statement in with block.

APPLY

Use apply for code blocks that don't return a value and mainly operate on the members of the receiver object. The common case apply is the object configuration. Such calls can be read as “apply the following assignments to the object.

Let's see how can we use apply in the above example:-

fun main() {
// Using 'apply' to configure the object
val person = Person().apply {
name = "Alice"
age = 30
job = "Engineer"
city = "New York"
}

// Printing the details
println("Name: ${person.name}, Age: ${person.age}, Job: ${person.job}, City: ${person.city}")
}

It executes a block of code on an object and then returns the object itself. It is useful when you want to modify an object and then continue to use that object.

We can see there are 2 main differences in with and apply .

Return Value:

apply returns the receiver object itself.

with returns the result of the block.

Use Case:

Use apply when you need to configure or initialize an object and continue using it.

Use with when you want to execute a block of code on an object and return a result from that block.

ALSO

also is used to perform additional actions on an object while returning the object itself. It’s often used for logging, validation, or other side effects without changing the object.

Let’s see by example:-

fun main() {
val person = Person().also {
// Perform actions like logging or validation
println("Setting properties for a new person.")
it.name = "Alice"
it.age = 30
it.job = "Engineer"
it.city = "New York"
}

// Printing the details
println("Name: ${person.name}, Age: ${person.age}, Job: ${person.job}, City: ${person.city}")
}

Run

run is used to execute a block of code on an object or a lambda expression and return the result of the block. It can be used both as a method on an object or as a standalone function.

Run using a standalone function

fun main() {
val result = run {
val a = 10
val b = 20
a + b // The result of the block is the sum of a and b
}

// Printing the result
println(result) // Output: 30
}

Using run with an Object:

fun main() {
val person = Person().run {
// Configure the object
name = "Alice"
age = 30
job = "Engineer"
city = "New York"
// Return a summary string
"Name: $name, Age: $age, Job: $job, City: $city"
}

// Printing the result
println(person) // Output: Name: Alice, Age: 30, Job: Engineer, City: New York
}

Let’s see a summary of what we have learned so far:-

Summary

  • let: Use for null-safe operations and transformations. Returns the result of the block.
  • apply: Use for configuring or initializing an object. Returns the receiver object itself.
  • run: Use for executing a block of code and obtaining a result. Can be used with or without an object.
  • also: Use for performing additional actions (e.g., logging) while keeping the original object unchanged. Returns the receiver object itself.
  • with: Use for performing multiple operations on an object and returning a result. Executes on a non-null object.

--

--