How do I chain multiple scope functions together in Kotlin?

In Kotlin, scope functions can be chained because each one returns either:

  • the receiver object: also, apply
  • the lambda result: let, run, with

The key is understanding what each function returns.

Common chaining pattern

val result = User("Alice")
    .also {
        println("Created user: $it")
    }
    .apply {
        name = name.uppercase()
    }
    .let {
        "User name is ${it.name}"
    }

println(result)

Here:

  1. also receives the object as it and returns the same User
  2. apply receives the object as this and returns the same User
  3. let receives the object as it and returns the lambda result, a String

Example with a data class

data class User(var name: String, var age: Int = 0)

val description = User("Alice")
    .apply {
        age = 30
    }
    .also {
        println("Configured user: $it")
    }
    .run {
        "$name is $age years old"
    }

println(description)

Output:

Configured user: User(name=Alice, age=30)
Alice is 30 years old

Choosing the right scope function in a chain

Function Object reference Returns Common use
let it lambda result transform value, null checks
run this lambda result compute result from object
with this lambda result operate on existing object
apply this receiver object configure object
also it receiver object side effects like logging

Nullable chaining

Scope functions are especially useful with nullable values:

val length = maybeName
    ?.trim()
    ?.takeIf { it.isNotEmpty() }
    ?.also {
        println("Valid name: $it")
    }
    ?.let {
        it.length
    }

Here, the chain stops if any step returns null.

Practical example

val user = User(" alice ")
    .apply {
        name = name.trim()
    }
    .also {
        println("After trim: $it")
    }
    .apply {
        name = name.replaceFirstChar { it.uppercase() }
    }
    .also {
        println("Final user: $it")
    }

Rule of thumb

Use:

apply { }

when you want to configure an object and keep chaining the same object.

Use:

also { }

when you want to perform a side effect and keep chaining the same object.

Use:

let { }

or:

run { }

when you want to transform the object into another result.

So a typical chain often looks like:

val result = createObject()
    .apply {
        // configure object
    }
    .also {
        // log or validate
    }
    .let {
        // transform into final result
    }

How do I use run, apply, also, and with for cleaner Kotlin code?

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 apply when the lambda modifies or configures the receiver.
  • Use also when the lambda performs side effects and should not affect the returned value.
  • Use run when the lambda produces a new value.
  • Use with when 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.

How do I combine multiple scope functions for concise Kotlin code?

In Kotlin, scope functions (like let, apply, also, run, and with) provide a way to execute a block of code within the context of an object. Combining them allows writing concise and readable code. Here’s how you can effectively use and combine them:

Example: Combining let and apply

Suppose you have an object that you need to both configure and process:

val result = myObject.apply {
    // Configure the object
    property1 = "value1"
    property2 = "value2"
}.let {
    // Use the configured object and transform it if needed
    it.someTransformingFunction()
}

Explanation:

  1. apply is used to mutate the object (configuration stage).
  2. let provides access to the configured object and performs further operations.

Example: Combining with and run

If you need to perform several actions on an object, including calculations:

val result = with(myObject) {
    // Perform actions on the object
    doAction1()
    doAction2()

    // Return something for further use
    this
}.run {
    // Execute more actions in the context of the returned object
    performAdditionalTask()
    finalResult()
}

Explanation:

  1. with is used as a concise way to operate on an object without changing its state, returning the object itself or another value.
  2. run is then used for further actions or transformations.

Example: Combining let and also

To perform logging or debugging while processing data:

val result = sourceString.let { input ->
    input.trim()
}.also { trimmed ->
    println("Trimmed string: $trimmed")
}

Explanation:

  1. let is used to create a pipeline where the string is transformed.
  2. also is used for side-effects, such as logging or debugging, without altering the object.

Example: Combining in Nested Chains

For a more complex scenario where multiple scope functions are needed:

val result = myObject.apply {
    property1 = "value1"
    property2 = "value2"
}.let {
    // Transform the configured object
    it.someTransformingFunction()
}.also {
    // Log the transformation
    println("Transformed object: $it")
}

This approach allows you to configure an object, transform it, and log its state in a single elegant chain.


General Guidelines

  • Use apply when you want to configure or initialize an object.
  • Use let when you want to execute a block of code with the object as a parameter and transform/compute something.
  • Use also when you need to perform a side effect (e.g., logging) while keeping the object unchanged.
  • Use run and with for blocks of code where you are mostly operating on the object and possibly returning a value.

By properly combining these scope functions, you achieve concise, clean, and functional Kotlin code!

How do I apply scope functions like let, also, and run for null safety in Kotlin?

In Kotlin, scope functions such as let, also, and run can be used effectively for handling null safety. These functions allow you to operate on nullable objects and execute a block of code only if the object is not null. Here’s how these scope functions can be applied for null safety:


1. let

The let scope function is triggered only if the object is non-null. It allows safe execution of a code block with the object as its context (it).

Usage for Null Safety:

val name: String? = "John"

name?.let {
    // Executes only if name is not null
    println("The name is $it")
}

If name is null, the let block will not be executed.


2. also

The also function is used for performing some side actions while retaining the original object. Use it when you want to ensure the object is non-null before performing the side effects.

Usage for Null Safety:

val name: String? = "John"

name?.also {
    // Executes only if name is not null
    println("Processing name: $it")
}

If name is null, the block inside also will not be executed.


3. run

The run function is used when you want to execute a block of code within the context of the object and return the result. It can also be used on nullable types with the safe call (?.).

Usage for Null Safety:

val name: String? = "John"

val length = name?.run {
    // Executes only if name is not null
    println("Calculating length of $this")
    this.length
}
println("Name length: $length")

If name is null, the run block will not be executed, and length will remain null.


Comparison and Use Cases:

Scope Function Purpose Nullable Handling Context Object
let Transform or execute action ?.let {} for null check it
also Side effect actions ?.also {} for null check it
run Configure or transform ?.run {} for null check this

Example Combining Null Safety with Scope Functions:

fun main() {
    val name: String? = "Alice"

    // Using let for null-safe operation
    name?.let { 
        println("Name is $it")
    }

    // Using also for additional actions
    name?.also { 
        println("Logging name: $it")
    }

    // Using run to transform and calculate length
    val length = name?.run { 
        println("Calculating length of $this")
        length
    }
    println("Length: $length")
}

Key Takeaways:

  1. Use ?.let {} to execute a block only if the object is non-null.
  2. Use ?.also {} to perform side effects without altering the object.
  3. Use ?.run {} to execute transformations or calculations within a null-safe block.

With these scope functions, Kotlin provides a clean and concise way to handle nullable types safely.