How to use Kotlin's 'it also let apply run'

Tan Jun Rong avatar

Tan Jun Rong

Kotlin is being officially used in Android development, and every Android developers are probably busy picking up Kotlin. That includes me.

I stumbled 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 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(pug: Pug): 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 the the re-write with let

val pug: Pug = Pug()
val hornyPug: HornyPug = pug.let { putHornOn(it) }

1b.Function definition

Now we know how to re-write it, it's time to take a look at Standard.kt for the how let and run is written:

public inline fun  T.let(block: (T) -> R): R = block(this)
public inline fun  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 = 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.apply(block: T.() -> Unit): T { block(); return this }
public inline fun  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().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

  1. create a file
  2. set the file to readable, writable, executable
  3. return the root path of the file

Without magical functions:

    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.

Anyhow, hope you enjoy the read! 😺📚☕


Thanks for reading, if you made it this far, I present you a....

Bonus Unicorn Pug.

All pugs are taken from freepik, no pugs are hurt in the making.

Tan Jun Rong avatar
Written By

Tan Jun Rong

Android Programmer who likes writing blogs, reading, coffee, snowboarding.
Published in Android
Enjoyed the post?

Clap to support the author, help others find it, and make your opinion count.