Skip to Content
Course content

208: Common Kotlin Interview Questions on Scope Functions

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've sat through plenty of technical interviews where the candidate knows the syntax of Kotlin, but they freeze the moment I ask, "Why would you use also instead of apply?" On paper, they look almost identical. In practice, using the wrong one can make your code read like a riddle. Let's actually break one and see where the lines are drawn.

data class UserSession(
    var username: String = "",
    var authToken: String = "",
    var isAuthenticated: Boolean = false
)

The "Configuration" phase

Suppose I have a raw UserSession that I need to set up before passing it to a service. My first instinct is usually apply. I want to "apply" these settings to the object.

val session = UserSession().apply {
    username = "dev_user"
    authToken = "abc-123"
    isAuthenticated = true
}
// 'session' is now a UserSession object

I'm inside the block, and I can access username directly. That's because apply uses this as the receiver. It's clean. But here's where the interview questions start: what if I want to log the session object while I'm initializing it, without confusing the configuration with the logging?

If I just put a println(this) inside apply, it works, but it feels "muddy." The apply block is for modifying the object. For side effects—things that don't change the object's state—I'll try also.

val session = UserSession().apply {
    username = "dev_user"
}.also {
    println("Session created for user: ${it.username}")
}

Notice the shift. In apply, I used this (implicitly). In also, I'm using it. This is a crucial distinction. also gives me the object as an argument, not as a receiver. In a real-world codebase, this prevents me from accidentally changing a value inside a block that's supposed to be "read-only" for logging or validation.

The return value trap

This is the part where most people stumble. I'm going to try and chain a let onto the end of my session setup to return a status message. Watch what happens to the type.

val result = UserSession().apply {
    username = "dev_user"
}.let {
    "User ${it.username} is now active"
}

println(result) // This prints the String, not the UserSession

I just changed the return type of the entire expression from a UserSession to a String. If I had used also instead of let, result would still be the UserSession object. In an interview, if you're asked the difference between let and also, don't just say "one uses it and one uses this." The real answer is about the return value.

  • apply and also return the object itself. (Great for chaining).
  • let and run return the result of the lambda. (Great for transformations).

Deciding between run and with

Now, what about run and with? They both use this. I'll try to use with to perform some operations on my session without assigning it to a variable first.

val session = UserSession("admin", "secret", true)

with(session) {
    println("Checking session for $username...")
    // I can access username directly because 'this' is the session
}

Now I'll try run. It looks almost exactly the same, but the syntax is slightly different.

session.run {
    println("Checking session for $username...")
}

Why do both exist? I've found that with is a standalone function—you pass the object in. run is an extension function. This means run is the winner when you need to handle nullability. I can't do with(session?) { ... } and expect it to skip the block if it's null. But I can do session?.run { ... }.

If I'm in an interview, I summarize it like this: with is for "with this object, do the following," while run is for "take this object and execute this logic on it, potentially returning a result."




📋 Practical Task

Refactoring the API Response Handler

You have a legacy piece of code that handles a nullable ApiResponse. It currently uses a messy set of if checks and temporary variables. Your goal is to refactor this into a clean chain using scope functions.

The Requirements:

  1. Use let to handle the nullability of the response.
  2. Inside that block, use apply to set the processedTimestamp of the response.
  3. Chain an also block to log the final processed response to the console.
  4. The final result of the entire expression should be the ApiResponse object itself (not a string or null).
data class ApiResponse(
    val data: String, 
    var processedTimestamp: Long = 0
)

fun handleResponse(response: ApiResponse?) {
    // TODO: Refactor the following logic into a single scope function chain
    if (response != null) {
        response.processedTimestamp = System.currentTimeMillis()
        println("Log: Processed response: ${response.data}")
        // Imagine this is passed to another function here
    }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.