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
orrun
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()
orperformIt()
- If there is transformation, we use
transform()
ortransformIt()
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!
All pugs are taken from freepik, no pugs are hurt in the making.
Top comments (4)
Thanks for that article. I'm currently learning Kotlin and this is some nice free extra knowledge.
I find some things in Kotlin named rather unfortunately (e.g.
var
+val
. The resemblence drives me crazy).However, as a beginner I got a rather beginner question about your writing: I noticed that you called those four methods "magical". To my understanding — coming from a PHP background — "magic" methods usually hold functionality that cannot be achieved by userland implementations.
This does not
apply
here, does it? You used "magical" purely as a rhetorical figure, right?thanks for reading <3
val
andvar
can be difficult to get used to at first, because you have to always decide it upfront, unlike in java-land. I remember it asval
beingvalue
andvar
beingvariable
. (so when you give it avalue
, it's fixed, and won't be able to change; whereasvariable
as the name suggest, it can be changed). Perhaps after knowing this it will be easier.Oh, on that.. it's actually half rhetorical... I say it 'magical' partly because it's so hard to understand. Another part is that it's because they are actually pretty interesting methods where I'm learning new concepts.
It's like a Mario pipe to me, where you pass in something into this pipe. Then, inside the pipe, something happened. When it comes out, it can be the same thing, or it can be a different thing.
Weird yet nice analogy. 🙈
And thanks for clarifying. :)
I was having a lot of problems understanding these topics. This helped a lot. You're the best. Thanks