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 plainBoolean
) - usingthis.isPresent
,this?.
orthis == false
in such situations feels awkward - it doesn’t put the result on a pedestal. In the call to
validate
, it is very likely thatresultOfDoingBusinessStuff
andsomeCalculatedValue
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.