Skip to content
ArceApps Logo ArceApps
ES

Mastering Kotlin Scope Functions: let, run, with, apply, also

⏱️ 4 min read
Mastering Kotlin Scope Functions: let, run, with, apply, also

🧐 The Problem They Solve (Language Design Theory)

In Java and other imperative languages, we often find ourselves repeating a variable name to perform multiple operations on it, or creating unnecessary temporary variables.

// Java Style
User user = new User();
user.setName("Alice");
user.setAge(25);
user.setEmail("alice@example.com");
repository.save(user);

Kotlin introduces Scope Functions to solve this by creating a temporary “mini-scope” where the context object is accessible implicitly (like this or it).

The Magic Quadrant

To understand them, don’t memorize. Understand the two dimensions that differentiate them:

  1. How do I access the object?
    • this: The object is the receiver of the lambda (implicit Extension Function).
    • it: The object is the argument of the lambda.
  2. What does the function return?
    • Context Object: Returns the object itself (good for chaining).
    • Lambda Result: Returns whatever the last line of the lambda returns (good for transformation).
Returns Context ObjectReturns Lambda Result
Object as thisapplyrun, with
Object as italsolet

🛠️ Deep Dive into Each Function

1. let: The Nullability Transformer

let is the Swiss Army knife. It takes it and returns the lambda result.

Main Use: Execute a block only if a variable is not null.

val user: User? = repository.findUser("123")

// Without let
if (user != null) {
    sendEmail(user.email)
}

// With let
user?.let {
    sendEmail(it.email)
}

Functional Pattern: Transformations.

val userDto = user?.let { userMapper.toDto(it) } ?: UserDto.Empty

2. apply: The Configurator

apply takes this and returns the object itself. Perfect for initializing objects or configuring builders.

Main Use: Post-construction initialization.

// Android Intent configuration
val intent = Intent(context, DetailActivity::class.java).apply {
    putExtra("ID", 123)
    putExtra("MODE", "EDIT")
    flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
// 'intent' is already configured and ready to use

3. run: The Execution Block

run is like let (returns result), but uses this.

Main Use: Compute a value based on object properties and return it.

val passwordHash = user.run {
    // I can access 'name' and 'email' directly without 'it'
    val salt = generateSalt(name)
    hash(email + salt)
}

There is also run without a receiver object (simply creates a scope).

val result = run {
    val x = 10
    val y = 20
    x + y
}

4. also: The Side Effect

also is like apply (returns object), but uses it. Its name says it all: “Do this… and also this other thing”.

Main Use: Logging or intermediate validations in a chain, without breaking the flow.

val user = createUser()
    .also { logger.info("User created: ${it.id}") } // Side effect
    .apply { role = "ADMIN" } // Configuration

If we used apply for the log, we would have to write logger.info("... ${this.id}"), which is valid but also makes it clearer that we are not modifying the object, just “looking” at it.

5. with: The Typing Saver

with is not an extension function, it is called as a normal function: with(object) { ... }.

Main Use: Grouping function calls on the same object.

with(binding) {
    titleView.text = "Hello"
    subtitleView.text = "World"
    submitButton.setOnClickListener { ... }
}

🚫 Anti-Patterns and Dangers

Scope Functions are addictive. Use them in moderation.

1. The Hell of Nested it (Shadowing)

user?.let {
    // it is user
    it.address?.let {
        // it is address, user is hidden
        it.city?.let {
             // it is city... Whose address was it?
        }
    }
}

Solution: Use explicit names in nested lambdas.

user?.let { user ->
    user.address?.let { address ->
        ...
    }
}

2. Mutating in let or run

If you are going to mutate the object’s state, use apply or also. If you use let, the reader expects a transformation, not a mutation. Be semantic.

3. Chains Too Long

If you chain 5 scope functions, the code becomes unreadable. Sometimes, a classic temporary variable is clearer and easier to debug.

🎯 Quick Decision Guide

  • Is it a null check? -> let
  • Is it configuring a new object? -> apply
  • Is it configuring and returning a different result? -> run
  • Is it an intermediate log? -> also
  • Is it grouping method calls? -> with

Mastering these functions allows you to write more idiomatic, expressive, and concise Kotlin code.

You might also be interested in

Null Safety in Kotlin: The End of the 'One Billion Dollar Mistake'
Kotlin September 10, 2025

Null Safety in Kotlin: The End of the 'One Billion Dollar Mistake'

Deeply understand Kotlin's type system, how it eliminates NullPointerExceptions, and advanced techniques for handling nullability elegantly.

Read more
The Billion Dollar Mistake: Null Safety in Kotlin vs Java
Kotlin September 10, 2025

The Billion Dollar Mistake: Null Safety in Kotlin vs Java

Why is 'null' so dangerous? Discover how Kotlin solves the NullPointerException problem at the compiler level and how to design null-safe APIs.

Read more
Clean Architecture: The Ultimate Guide for Modern Android
Architecture October 15, 2025

Clean Architecture: The Ultimate Guide for Modern Android

Demystifying Clean Architecture: A deep dive into layers, dependencies, and data flow to build indestructible Android apps.

Read more