Kotlin magick
Kotlin is being officially used in Android development, and every Android developers are probably busy picking up Kotlin. That includes me.
print "hello world"
I stumbled upon these few magical methods during my Kotlin journey:
class Animal {}
class Dog extends Animal {}
.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 focused. 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 been 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 = pug.apply { bark() }
Now, using also
val pug: Pug = Pug()
val stillPug: Pug = pug.also { it.bark() }
2b. function definition
Take a look at the Standard.kt
on 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().performIt {
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) }
}
With the renaming, it's easier to understand, don't you think so?
It is at least for me.
Thanks for reading, if you made it this far, I present you a....
All pugs are taken from freepik, no pugs are hurt in the making.