ProgrammingKotlin developer

What are the standard higher-order functions apply, also, let, and run in Kotlin, how do they differ from each other and what are they used for?

Pass interviews with Hintsage AI assistant

Answer.

In Kotlin, the standard higher-order functions apply, also, let, and run were introduced to simplify chainable configuration of objects and local modifications with minimal boilerplate code. These functions facilitate work with mutable and immutable objects, allow for concise expression of transformation chains, and alleviate some issues with variable scope.

Background

These functions were derived from the builder pattern and fluent interface approaches. Their introduction is attributed to the desire to make code cleaner and free from excessive declaration of auxiliary variables.

Problem

The traditional approach requires multiple accesses to the object or extracting temporary variables. This reduces readability and increases the risk of errors:

val user = User() user.name = "Alex" user.age = 26 user.isActive = true

Solution

Using the functions apply, also, let, and run increases expressiveness:

val user = User().apply { name = "Alex" age = 26 isActive = true }

Brief descriptions:

  • apply: returns the calling object (this), used for configuration.
  • also: for side effects, returns the object, the lambda argument is it.
  • let: for transforming values (e.g., for nullable types), returns the result of the lambda (final value).
  • run: combines the capabilities of apply and let. Works with this inside the lambda and returns the result of the lambda.

Code example:

data class User(var name: String = "", var age: Int = 0, var isActive: Boolean = false) val configuredUser = User().apply { name = "Alice" age = 30 isActive = true } debugUser(configuredUser.also { println("User is configured: $it") }) val emailLength = configuredUser.email?.let { it.length } ?: 0 val description = configuredUser.run { "$name ($age)" }

Key features:

  • apply, also, let, and run are important tools for concise and expressive work with objects.
  • Context inside the lambda: apply/run — this, let/also — it.
  • Their correct usage simplifies code and reduces the risk of errors during object configuration or validation.

Tricky Questions.

Can the let function modify the object it works with?

The let function is not meant to modify the object. It is more about applying a transformation to the value and returning the result. It’s important to remember that inside let, it refers to the value, not this.

val upperName = user.name.let { it.uppercase() } // let does not modify user

What is the difference between apply and run?

Both work with the context this inside the lambda, the difference lies in the returned value:

  • apply returns the object itself
  • run returns the result of the lambda execution
// apply val building = StringBuilder().apply { append("start-") append("end") } // building is a StringBuilder // run val result = StringBuilder().run { append("start-") append("end") toString() } // result is a String

Can apply and let be nested? If so, when is this justified?

Yes, nesting is gently recommended for aggregation or step-by-step configuration of objects, especially when working with nullable:

val userInfo = user?.apply { isActive = true }?.let { "${it.name} is active: ${it.isActive}" }

Typical mistakes and anti-patterns

  • Confused contexts (this/it), leading to unexpected logic.
  • Using apply for operations that do not modify the object.
  • Excessive nesting and mixing of functions, reducing readability.

Real-life examples

Negative case

Let is used everywhere in the code for modifying the object, mixing apply and let — as a result, the object does not change where expected, and its value is lost in the chains.

Pros:

  • Compactness of code

Cons:

  • Easy to make mistakes in the returned value and produce unexpected side effects
  • Hard to maintain the code and find bugs

Positive case

Apply and also are used for configuration and logging, let only for working with nullable and obtaining results, run — for transformation chains needing a computed new value.

Pros:

  • Easy to read and maintain
  • Clear correspondence to the purpose of the functions

Cons:

  • Requires knowledge of the nuances of each scope function