🚀 · also() vs. apply()

4 min read · Updated on by

Read on for a thorough discussion on the differences between also() and apply(), when to use which, and how to use them to show future readers what’s important.

In the previous article, we introduced the apply() and also() scope functions, and discovered that they were nearly identical.

The following are functionally identical.


//sampleStart
class A {
    fun doStuff() {
        // Stuff
    }
}

A().also { it.doStuff() }

A().apply { doStuff() }
//sampleEnd

So when should we use which, and why? The difference between the two, as you might have guessed, is in the intent — how a reader interprets your code based on which function you use.

In general, you would use also when you need to do "something on the side" with an object, but then want to continue working with it when you're done. Often, using also communicates that what's happening there isn't part of the essence of what you're implementing - it's more like "hey, BTW, also do this". Validations, logging, etc. often fall into this category. The documentation states that also should be used for "additional effects", which seems to express the same thing.

Another situation where also can…also…be considered (as opposed to apply) is:

  • the effect isn’t closely tied to the receiver of also
  • readability would benefit from introducing an explicit name.

The following illustrates both:


fun calculateSomeValue() = Unit
fun doBusinessStuff() = Unit
fun compatible(one: Any, two: Any) = true
//sampleStart
val someCalculatedValue = calculateSomeValue()
val result1 = doBusinessStuff().also { resultOfDoingBusinessStuff ->
    if(!compatible(resultOfDoingBusinessStuff, someCalculatedValue)) { 
        throw Exception("Incompatible values detected!") 
    }
}

val result2 = doBusinessStuff().apply { 
    if(!compatible(this, someCalculatedValue)) { 
        throw Exception("Incompatible values detected!") 
    }
}
//sampleEnd
fun main() {
    val poem = """
        When you're in the dance of code's ballet,
        Kotlin's syntax is the dancer so fey.
        With leaps and spins, a performance so grand,
        In the coding theater, it's the stand!
    """.trimIndent()
    println(poem)
}

I feel the first way is better for two reasons:

  • it names the result, which gives us a better idea of what we’re working with. This would be especially true if the result somehow communicated success or failure (e.g. using Optional or a nullable or just a plain Boolean) - using this.isPresentthis?. or this == false in such situations feels awkward
  • it doesn’t put the result on a pedestal. In the call to validate, it is very likely that resultOfDoingBusinessStuff and someCalculatedValue are "on the same level" which gets expressed much better in the first way than in the second

In contrast, I would consider using apply in the opposite scenarios, i.e. when the effect actually is a key part of what's going on or when the effect is intimately tied to the receiver.

This goes well with the documentation, that states apply should be used for object configuration.

Take a look at the following:


interface Details
interface UserDetails {
    val details: Details
}
data class CustomAuthenticationToken(val user: UserDetails, val code: String) {
    lateinit var details: Details
} 
//sampleStart
fun createSuccessAuthentication(user: UserDetails , token: CustomAuthenticationToken): CustomAuthenticationToken {
    val result = CustomAuthenticationToken(
        user = user,
        code = token.code,
    )
    result.details = user.details
    return result
}

fun createSuccessAuthenticationWithApply(user: UserDetails , token: CustomAuthenticationToken) = 
    CustomAuthenticationToken(
        user = user,
        code = token.code,
    ).apply {
        details = user.details
    }
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the navigator in code's great quest,
        With extension functions, it performs the best.
        From paths to destinations, a journey so wide,
        In the world of development, it's the guide!
    """.trimIndent()
    println(poem)
}

We managed to both save some keystrokes, redefine the function as an expression, therefore taking advantage of type inference for the return value, and also arguably made the code more elegant.

Summary

I would say that the main thing I think about when deciding whether to use apply or also is how the block of code relates to the receiver, and to the scenario I’m implementing. Is it a key part of what’s going on? Then I would consider using apply. Is it just a “side-gig”, a “chore” that has no bearing on the essence of the calculation itself? That’s when I consider using also.

Doing this has the major benefit of immediately telling a future maintainer what to concentrate on — in a sense, we’re emphasizing the happy path. When trying to understand what’s going on in a piece of code, and what the author was thinking when they were writing it, it is handy to be able to say “oh, okay, so the author didn’t consider this an essential part of what’s going on here, so let me ignore it for now and concentrate on what they thought was important for me to understand, and then I can come back and deal with the details”. Keep this in mind when writing your own code.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

The Kotlin Primer