ProgrammingMiddle Kotlin Developer

What are scope functions in Kotlin (let, also, run, apply, with)? What are the differences between these functions, how to choose them for different tasks, and what nuances may arise during their use? Provide examples.

Pass interviews with Hintsage AI assistant

Answer.

Scope functions in Kotlin are standard functions (let, also, run, apply, with) that allow managing the execution context of a block of code for an object. They differ in:

  • return type,
  • how the object is accessed within the block: through it or this.

Brief comparison:

Functionthis/itReturnsFor what?
letitresultchain of operations, working with nullable, map
alsoitobjectside effects, logging, debug
runthisresultcalculations, initialization with return
applythisobjectobject configuration, builders
withthisresultworking with external APIs, object "outside"

Examples:

  • let: convenient if the object is nullable:
val str: String? = "Text" str?.let { println(it.length) }
  • apply: configuring an object:
val paint = Paint().apply { color = Color.RED strokeWidth = 2f }
  • run: executing on an object, returning the result:
val length = "abcde".run { length }
  • with: for working with an external object:
val sb = StringBuilder() with(sb) { append("Hello, ") append("world!") toString() }
  • also: for side effects (e.g., logs):
val list = mutableListOf(1, 2, 3) list.also { println("Before: $it") }.add(4)

Key points to consider:

  • let creates a copy of the object in it, changing object properties is not very convenient.
  • apply and also always return the object itself (this / it), useful for builders.
  • run/with are often confused: with is a regular function, not an extension.

Trick question.

What is the difference between let and also?

Answer:

  • Both use it inside the block,
  • let returns the result of the lambda, often used for transformation chains,
  • also returns the original object, used for side effects (log, debug) to avoid interfering with the transformation chain.

Example:

val result = listOf(1).also { println(it) }.map { it * 2 } // result — List<Int>

Examples of real mistakes due to lack of knowledge of nuances:


Story

A beginner used let to configure an object, thinking that this way it would change its state in a "chain." As a result, upon completion of the configuration block, they obtained not an object but the result of the lambda (e.g., nothing), violating the DSL building chain.


Story

When writing code to work with nullable objects, run was used instead of let, not noticing the difference in return value. As a result, the expression's value differed from expected, resulting in null where it shouldn't be — breaking the application's logic.


Story

In a large builder, with was accidentally used for internal objects, expecting the extension pattern. Since with is not an extension function, the chain of several with blocks did not work correctly, internal calls got confused and went beyond the actual object. The hierarchy of object creation had to be completely rewritten.