Back to blog
July 21, 2025
3 min read

Kotlin's 'let', 'run', 'apply', 'also', 'with'

Scope functions demystified now

Kotlin’s Scope Functions Demystified

Kotlin’s scope functions (let, run, apply, also, and with) provide a way to execute a block of code within the context of an object.

Understanding Scope Functions

let

// Returns the result of the lambda
val result = user.let {
    it.name = "John"
    it.age = 30
    it.toString()
}

// Null safety
user?.let {
    // Only executed if user is not null
    processUser(it)
}

run

// Returns the result of the lambda
val result = user.run {
    name = "John"
    age = 30
    toString()
}

// With receiver
val result = run {
    val user = User()
    user.name = "John"
    user.age = 30
    user
}

apply

// Returns the receiver object
val user = User().apply {
    name = "John"
    age = 30
    address = "New York"
}

// Builder pattern
val button = Button().apply {
    text = "Click me"
    onClick = { /* ... */ }
    isEnabled = true
}

also

// Returns the receiver object
val user = User().also {
    println("Created user: $it")
    it.validate()
}

// Side effects
val numbers = mutableListOf<Int>().also {
    it.add(1)
    it.add(2)
    it.add(3)
    println("Added numbers: $it")
}

with

// Returns the result of the lambda
val result = with(user) {
    name = "John"
    age = 30
    toString()
}

// Multiple operations
with(database) {
    beginTransaction()
    try {
        insert(user)
        commit()
    } catch (e: Exception) {
        rollback()
        throw e
    }
}

Best Practices

  1. Use let for null safety and transforming values
  2. Use run for object configuration and computing a result
  3. Use apply for object configuration and building
  4. Use also for side effects and logging
  5. Use with for grouping function calls on an object

Common Patterns

Null Safety

// Using let
user?.let { safeUser ->
    processUser(safeUser)
}

// Using run
user?.run {
    name = "John"
    age = 30
    save()
}

Builder Pattern

// Using apply
val dialog = AlertDialog.Builder(context).apply {
    setTitle("Title")
    setMessage("Message")
    setPositiveButton("OK") { _, _ -> }
    setNegativeButton("Cancel") { _, _ -> }
}.create()

// Using also
val button = Button(context).also {
    it.text = "Click me"
    it.setOnClickListener { /* ... */ }
    it.isEnabled = true
}

Function Chaining

// Using let
val result = user.let { it.name }
    .let { it.toUpperCase() }
    .let { it.trim() }

// Using run
val result = user.run { name }
    .run { toUpperCase() }
    .run { trim() }

Performance Considerations

  • Scope functions have minimal runtime overhead
  • Choose the right function for your use case
  • Avoid deep nesting of scope functions
  • Consider readability over brevity

Common Mistakes

  1. Using apply when you need a return value
  2. Using let when run would be more appropriate
  3. Nesting too many scope functions
  4. Using scope functions when simple property access would suffice

Conclusion

Scope functions are powerful tools in Kotlin that can make your code more concise and readable. Choose the right function based on your specific use case and follow best practices for optimal results.