Kotlin is being officially used in Android development, and every Android developers are probably busy picking up Kotlin. That includes me.
I stumble upon these few magical methods during my Kotlin journey:
.also() .let() .apply() .run()
They are magical because they can perform some Kotlin magics and at the same time greatly resemble English words. Thanks to the resemblance, I even tried forming sentence using them. Let’s assume Apply is a person name, I can make a grammatically correct English sentence with them: it also let Apply run.
Nonsense apart, I find it really hard to understand the usage based on their names.
There are also with and other friends in the Standard.kt, but I want to keep this post focus. So I’m leaving out the rest. Actually I’m just lazy to cover them all ಠ_ಠ. I want to go do some snowboarding instead, it’s winter already, yay! ^3^
1. let and run transform
1a. pug analogy, part I
There’s a famous saying “to begin learning is to begin to forget”. So let’s forget about it also let apply run for a second. Ok, I just made that up. Let’s start with a simple requirement.
Let’s say you have a pug.
and you want to add a horn to it.
Here’s the code for doing this.
val pug: Pug = Pug() val hornyPug: HornyPug = putHornOn(pug)
fun putHornOn(): HornyPug { // put horn logic return hornyPug }
Now it has became a pug with horn, let’s call it hornyPug:
From pug to hornyPug, the original pug has changed. I call this “transformation”.
Let’s re-write this using run
val pug: Pug = Pug() val hornyPug: HornyPug = pug.run { putHornOn(this) }
Here’s re-write with let
val pug: Pug = Pug() val hornyPug: HornyPug = pug.let { putHornOn(it) }
1b.Function definition
Take a look at the Standard.kt for the how let and run is written:
public inline fun <T, R> T.let(block: (T) -> R): R = block(this) public inline fun <T, R> T.run(block: T.() -> R): R = block()
It can be hard to read at first, let’s only focus on the return type for now:
- R is the return type
- T is the input or type of the calling object.
What it means is T type will turn into R type after let or run.
In the case of our example, pug is T, hornyPug is R.
1c. Key take away:
- whenever transformation happens, use let or run
2. apply and also doesn’t transform
2a. pug analogy, part II
Let’s do the same thing for this.
Say you have a pug in a trash can. (hint: trash can is not important)
You want it to bark(): “woof!”
After barking, it’s still the same old pug.
Here’s the code:
val pug: Pug = Pug() pug.bark() // after barking, pug is still pug, nothing changes
class Pug { fun bark() { // Log.d("pug", "woof!") // print log to Android Studio // no return, which means, return Unit in Kotlin } }
Before and after .bark(), pug is still pug, nothing changes.
Let’s re-write this using apply
val pug: Pug = Pug() val stillPug = pug.apply { bark() }
Now, using also
val pug: Pug = Pug() val stillPug = pug.also { it.bark() }
2b. function definition
Take a look at the Standard.kt for the how apply and also are written:
public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this } public inline fun <T> T.also(block: (T) -> Unit): T { block(this); return this }
Notice that now it doesn’t have R type, because T the original object type, is returning T after apply or also.
In our case, T is pug, and it remains the same before and after.
2c. Key take away
When there is no transformation, use apply or also.
3. A little confusing, how about renaming?
Most of the developers who I talked to find it also let apply run naming to be confusing. I am wondering if it would be easier to understand if we have a better naming?
Let’s try this, Kotlin allows us to import a method name as another name.
import kotlin.apply as perform import kotlin.run as transform import kotlin.also as performIt import kotlin.let as transformIt
Explanation:
- If there is no transformation, we use perform() or performIt()
- If there is transformation, we use transform() or transformIt()
Let’s check the example use case.
3a. configuration example – perform()
If we need to create a file, and configure it:
val file = File() file.setReadable(true) file.setExecutable(true) file.setWritable(true)
In the code above, we configure file by running 3 lines of code. At the end, file doesn’t change into something else. So no transformation. We use the perform version.
File().perform { setReadable(true) setExecutable(true) setWritable(true) }
In this case, performIt will work too:
File().perform { it.setReadable(true) it.setExecutable(true) it.setWritable(true) }
But perform is better, since we don’t really need it
3b. perform task on an object – performIt()
If we need to perform a task on an object, for example, when a crash happens, we want to send the user.id, user.name, and user.country to Crashlytics.
In this case, there is no transformation going on. I choose the performIt()version.
user.performIt { Crashlytics.sendId(it.id) Crashlytics.sendName(it.name) Crashlytics.sendCountry(it.country) }
The perform() will work too.
user.perform { Crashlytics.sendId(id) Crashlytics.sendName(name) Crashlytics.sendCountry(country) }
It’s a matter of preference, whether to choose perform or performIt. I don’t think we should waste too much time thinking about which to be chosen.
3c. creating view holder – transform
Let’s say we have a method to create ViewHolder.
fun create(parent: ViewGroup): PugViewHolder { val itemView = LayoutInflater.from(parent.context).inflate(R.layout.item_pug, parent, false) return PugViewHolder(itemView) }
We can see that itemView is transformed into PugViewHolder at the end. So we can use the transformIt version.
fun create(parent: ViewGroup): PugViewHolder { return LayoutInflater.from(parent.context).inflate(R.layout.item_pug, parent, false).transformIt { PugViewHolder(it) } }
Again, the transform() version will work too. So I’m not writing 3d.
4. All working together
Consider a case where we need to
- create a file
- set the file to readable, writable, executable
- return the root path of the file
fun createFile_setMode_returnRootPath(): String { val file = File() file.setReadable(true) file.setExecutable(true) file.setWritable(true) val rootPath = findRootPath(file) return rootPath }
re-write using magic functions:
fun createFile_setMode_returnRootPath(): String { return File() .perform { setReadable(true) setExecutable(true) setWritable(true) } .transformIt { findRootPath(it) } }
Hope it helps!
Bonus Unicorn Pug.
All pugs are taken from freepik, no pugs are hurt in the making.
Source: https://dev.to/worker8/how-to-use-kotlins-it-also-let-apply-run-301b