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 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!