Example of when should we use run, let, apply, also and with on Kotlin

let, also, apply, takeIf, takeUnless are extension functions in Kotlin.

To understand these function you have to understand Extension functions and Lambda functions in Kotlin.

Extension Function:

By the use of extension function, we can create a function for a class without inheriting a class.

Kotlin, similar to C# and Gosu, provides the ability to extend a class with new functionality without having to inherit from the class or use any type of design pattern such as Decorator. This is done via special declarations called extensions. Kotlin supports extension functions and extension properties.

So, to find if only numbers in the String, you can create a method like below without inheriting String class.

fun String.isNumber(): Boolean = this.matches("[0-9]+".toRegex())

you can use the above extension function like this,

val phoneNumber = "8899665544"
println(phoneNumber.isNumber)

which is prints true.

Lambda Functions:

Lambda functions are just like Interface in Java. But in Kotlin, lambda functions can be passed as a parameter in functions.

Example:

fun String.isNumber(block: () -> Unit): Boolean {
    return if (this.matches("[0-9]+".toRegex())) {
        block()
        true
    } else false
}

You can see, the block is a lambda function and it is passed as a parameter. You can use the above function like this,

val phoneNumber = "8899665544"
    println(phoneNumber.isNumber {
        println("Block executed")
    })

The above function will print like this,

Block executed
true

I hope, now you got an idea about Extension functions and Lambda functions. Now we can go to Extension functions one by one.

let

public inline fun <T, R> T.let(block: (T) -> R): R = block(this)

Two Types T and R used in the above function.

T.let

T could be any object like String class. so you can invoke this function with any objects.

block: (T) -> R

In parameter of let, you can see the above lambda function. Also, the invoking object is passed as a parameter of the function. So you can use the invoking class object inside the function. then it returns the R (another object).

Example:

val phoneNumber = "8899665544"
val numberAndCount: Pair<Int, Int> = phoneNumber.let { it.toInt() to it.count() }

In above example let takes String as a parameter of its lambda function and it returns Pair in return.

In the same way, other extension function works.

also

public inline fun <T> T.also(block: (T) -> Unit): T { block(this); return this }

extension function also takes the invoking class as a lambda function parameter and returns nothing.

Example:

val phoneNumber = "8899665544"
phoneNumber.also { number ->
    println(number.contains("8"))
    println(number.length)
 }

apply

public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }

Same as also but the same invoking object passed as the function so you can use the functions and other properties without calling it or parameter name.

Example:

val phoneNumber = "8899665544"
phoneNumber.apply { 
    println(contains("8"))
    println(length)
 }

You can see in the above example the functions of String class directly invoked inside the lambda funtion.

takeIf

public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? = if (predicate(this)) this else null

Example:

val phoneNumber = "8899665544"
val number = phoneNumber.takeIf { it.matches("[0-9]+".toRegex()) }

In above example number will have a string of phoneNumber only it matches the regex. Otherwise, it will be null.

takeUnless

public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? = if (!predicate(this)) this else null

It is the reverse of takeIf.

Example:

val phoneNumber = "8899665544"
val number = phoneNumber.takeUnless { it.matches("[0-9]+".toRegex()) }

number will have a string of phoneNumber only if not matches the regex. Otherwise, it will be null.

You can view similar answers which is usefull here difference between kotlin also, apply, let, use, takeIf and takeUnless in Kotlin


All these functions are used for switching the scope of the current function / the variable. They are used to keep things that belong together in one place (mostly initializations).

Here are some examples:

run - returns anything you want and re-scopes the variable it's used on to this

val password: Password = PasswordGenerator().run {
       seed = "someString"
       hash = {s -> someHash(s)}
       hashRepetitions = 1000

       generate()
   }

The password generator is now rescoped as this and we can therefore set seed, hash and hashRepetitions without using a variable. generate() will return an instance of Password.

apply is similar, but it will return this:

val generator = PasswordGenerator().apply {
       seed = "someString"
       hash = {s -> someHash(s)}
       hashRepetitions = 1000
   }
val pasword = generator.generate()

That's particularly useful as a replacement for the Builder pattern, and if you want to re-use certain configurations.

let - mostly used to avoid null checks, but can also be used as a replacement for run. The difference is, that this will still be the same as before and you access the re-scoped variable using it:

val fruitBasket = ...

apple?.let {
  println("adding a ${it.color} apple!")
  fruitBasket.add(it)
}

The code above will add the apple to the basket only if it's not null. Also notice that it is now not optional anymore so you won't run into a NullPointerException here (aka. you don't need to use ?. to access its attributes)

also - use it when you want to use apply, but don't want to shadow this

class FruitBasket {
    private var weight = 0

    fun addFrom(appleTree: AppleTree) {
        val apple = appleTree.pick().also { apple ->
            this.weight += apple.weight
            add(apple)
        }
        ...
    }
    ...
    fun add(fruit: Fruit) = ...
}

Using apply here would shadow this, so that this.weight would refer to the apple, and not to the fruit basket.


Note: I shamelessly took the examples from my blog


There are 6 different scoping functions:

  1. T.run
  2. T.let
  3. T.apply
  4. T.also
  5. with
  6. run

I prepared a visual note as the below to show the differences :

data class Citizen(var name: String, var age: Int, var residence: String)

enter image description here

Decision depends on your needs. The use cases of different functions overlap, so that you can choose the functions based on the specific conventions used in your project or team.

Although the scope functions are a way of making the code more concise, avoid overusing them: it can decrease your code readability and lead to errors. Avoid nesting scope functions and be careful when chaining them: it's easy to get confused about the current context object and the value of this or it.

Here is another diagram for deciding which one to use from https://medium.com/@elye.project/mastering-kotlin-standard-functions-run-with-let-also-and-apply-9cd334b0ef84 enter image description here

Some conventions are as the following :

Use also for additional actions that don't alter the object, such as logging or printing debug information.

val numbers = mutableListOf("one", "two", "three")
 numbers
 .also { println("The list elements before adding new one: $it") }
 .add("four")

The common case for apply is the object configuration.

val adam = Person("Adam").apply {
age = 32
city = "London"        
}
println(adam)

If you need shadowing, use run

fun test() {
    var mood = "I am sad"

    run {
        val mood = "I am happy"
        println(mood) // I am happy
    }
    println(mood)  // I am sad
}

If you need to return receiver object itself, use apply or also


There are a few more articles like here, and here that are worth to take a look.

I think it is down to when you need a shorter, more concise within a few lines, and to avoid branching or conditional statement checking (such as if not null, then do this).

I love this simple chart, so I linked it here. You can see it from this as written by Sebastiano Gottardo.

enter image description here

Please also look at the chart accompanying my explanation below.

Concept

I think it as a role playing way inside your code block when you call those functions + whether you want yourself back (to chain call functions, or set to result variable, etc).

Above is what I think.

Concept Example

Let's see examples for all of them here

1.) myComputer.apply { } means you want to act as a main actor (you want to think that you're computer), and you want yourself back (computer) so you can do

var crashedComputer = myComputer.apply { 
    // you're the computer, you yourself install the apps
    // note: installFancyApps is one of methods of computer
    installFancyApps() 
}.crash()

Yup, you yourself just install the apps, crash yourself, and saved yourself as reference to allow others to see and do something with it.

2.) myComputer.also {} means you're completely sure you aren't computer, you're outsider that wants to do something with it, and also wants it computer as a returned result.

var crashedComputer = myComputer.also { 
    // now your grandpa does something with it
    myGrandpa.installVirusOn(it) 
}.crash()

3.) with(myComputer) { } means you're main actor (computer), and you don't want yourself as a result back.

with(myComputer) {
    // you're the computer, you yourself install the apps
    installFancyApps()
}

4.) myComputer.run { } means you're main actor (computer), and you don't want yourself as a result back.

myComputer.run {
    // you're the computer, you yourself install the apps
    installFancyApps()
}

but it's different from with { } in a very subtle sense that you can chain call run { } like the following

myComputer.run {
    installFancyApps()
}.run {
    // computer object isn't passed through here. So you cannot call installFancyApps() here again.
    println("woop!")
}

This is due to run {} is extension function, but with { } is not. So you call run { } and this inside the code block will be reflected to the caller type of object. You can see this for an excellent explanation for the difference between run {} and with {}.

5.) myComputer.let { } means you're outsider that looks at the computer, and want to do something about it without any care for computer instance to be returned back to you again.

myComputer.let {
    myGrandpa.installVirusOn(it)
}

The Way to Look At It

I tend to look at also and let as something which is external, outside. Whenever you say these two words, it's like you try to act up on something. let install virus on this computer, and also crash it. So this nails down the part of whether you're an actor or not.

For the result part, it's clearly there. also expresses that it's also another thing, so you still retain the availability of object itself. Thus it returns it as a result.

Everything else associates with this. Additionally run/with clearly doesn't interest in return object-self back. Now you can differentiate all of them.

I think sometimes when we step away from 100% programming/logic-based of examples, then we are in better position to conceptualize things. But that depends right :)

Tags:

Kotlin