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

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

  1. create a file
  2. set the file to readable, writable, executable
  3. 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-...