DEV Community

Tan Jun Rong
Tan Jun Rong

Posted on

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()
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode
fun putHornOn(): HornyPug {
   // put horn logic
   return hornyPug
}
Enter fullscreen mode Exit fullscreen mode

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) }
Enter fullscreen mode Exit fullscreen mode

Here's re-write with let

val pug: Pug = Pug()
val hornyPug: HornyPug = pug.let { putHornOn(it) }
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
class Pug {
    fun bark() {
        // Log.d("pug", "woof!") // print log to Android Studio
        // no return, which means, return Unit in Kotlin
    }
}
Enter fullscreen mode Exit fullscreen mode

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() }
Enter fullscreen mode Exit fullscreen mode

Now, using also

val pug: Pug = Pug()
val stillPug = pug.also { it.bark() }
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
    }
Enter fullscreen mode Exit fullscreen mode

In this case, performIt will work too:

    File().perform {
        it.setReadable(true)
        it.setExecutable(true)
        it.setWritable(true)
    }
Enter fullscreen mode Exit fullscreen mode

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)
    }
Enter fullscreen mode Exit fullscreen mode

The perform() will work too.

    user.perform {
        Crashlytics.sendId(id)
        Crashlytics.sendName(name)
        Crashlytics.sendCountry(country)
    }
Enter fullscreen mode Exit fullscreen mode

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)
    }
Enter fullscreen mode Exit fullscreen mode

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)
        }
    }
Enter fullscreen mode Exit fullscreen mode

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
    }
Enter fullscreen mode Exit fullscreen mode

re-write using magic functions:

    fun createFile_setMode_returnRootPath(): String {
        return File()
            .perform {
                setReadable(true)
                setExecutable(true)
                setWritable(true)
            }
            .transformIt { findRootPath(it) }
    }
Enter fullscreen mode Exit fullscreen mode

Hope it helps!

Bonus Unicorn Pug.

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

Top comments (4)

Collapse
 
loilo profile image
Florian Reuschel

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?

Collapse
 
worker8 profile image
Tan Jun Rong

thanks for reading <3

val and var can be difficult to get used to at first, because you have to always decide it upfront, unlike in java-land. I remember it as val being value and var being variable. (so when you give it a value, it's fixed, and won't be able to change; whereas variable as the name suggest, it can be changed). Perhaps after knowing this it will be easier.

I noticed that you called those four methods "magical".

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.

Collapse
 
loilo profile image
Florian Reuschel

Weird yet nice analogy. 🙈

And thanks for clarifying. :)

Collapse
 
pavandotexe profile image
Pavan

I was having a lot of problems understanding these topics. This helped a lot. You're the best. Thanks