Introduction

Several documents can be found online describing the standard Kotlin functions. Some explain the functions quite well. Unfortunately most of them do not go into details too much detail on when to use them. This article is intended to clear that up a little. So lets first see which functions I'll talk about:

Function Signature Receiver Returns
let fun <T, R> T.let(block: (T) -> R): R it Block result
also fun <T> T.also(block: (T) -> Unit): T it Receiver (it)
apply fun <T> T.apply(block: T.() -> Unit): T this `this` value
run fun <T, R> T.run(block: T.() -> R): R this Block result
with fun <T, R> with(receiver: T, block: T.() -> R): R this Block result

Now let's discuss each function in detail.

Let

Let (fun <T, R> T.let(block: (T) -> R): R) runs the given block with it as its receiver, and returns the result. This is most useful for two things:

  1. Conditionally execute something on a nullable object
  2. Chaining calls and transformations on the given object

Most uses I've seen so far are to conditionally run something on a nullable object, for example like this:

              
val nullable: Any? = doSomething()
nullable?.let { view.setText(it.text) } 
            

Another use that I see less in code is to use it to chain operations and transformations. It is very powerful and I think this is often overlooked. This would look something like this:

              
fun getImage() =
    // returns a base 64 encoded, encrypted image image stream
    // FIXME: use with
    loadImage()
        // decode it
        ?.let { decodeBase64(it) }
        // decrypt it
        ?.let { decrypt(it) }
        ?.let { scale(it) }
            

So as you can see the function getImage, first loads a base64 encoded, encrypted image stream from somewhere. If this result is not null, it will first base64 decode the image, then it will decrypt the image and finally it will scale the image according to some specifications. The result, or null, will be returned by this function.

Also

Also (fun <T> T.also(block: (T) -> Unit): T) is a lot like let, with one small difference. Instead of returning something from the block, it will return the receiver itself. This means that it can be used to perform an additional action to its input, before processing it any further. This means it can be used for the same thing as let in our first let example:

              
val nullable: Any? = doSomething()
nullable?.let { view.setText(it.text) } 
            

But more importantly it can be used to perform additional actions on the same object. So returning to the example of loading, decrypting and scaling the image, it can be used to perform additional actions on the object:

              
fun getImage(id: String) =
    // returns a base 64 encoded, encrypted image image stream
    loadImage(id)
        // decode it
        ?.let { decodeBase64(it) }
        // decrypt it
        ?.let { decrypt(it) }
        // save this result in some cache
        ?.also { cacheResult(id, it) }
        ?.let { scale(it) }
            

Above code will besides just decrypting and scaling it, also cache the decrypted result in some sort of cache. Even if fun cacheResult(): Unit returns nothing. After saving it in the cache it will scale the image and return it.

Apply

Apply (fun <T> T.apply(block: T.() -> Unit): T) runs on an object (referenced by this) and returns the object itself. It's most useful for post initialization of an object.

              
fun tapSomeBeer(size: Size) =
    Beer().apply {
        this.size = size
        isSpecialBeer = true
        Logger.log("I just got some beer 🍺🍺")
    }
            

So as you can see, we just create an instance and do some post initialization on it.

Run

Run (fun <T, R> T.run(block: T.() -> R): R) executes the given code with this as its receiver. It returns the result from the block. So, let's get some more beer:

              
fun getMoreBeer() =
    // Find the jummy 🍺
    BeerStorage.lookup(JUMMY_BEER).run { takeNext() }
            

As you can see, this is a lot like apply; the only difference is that apply returns the this object. So run is useful when you need to transform the object to something else, and when you want to perform some other actions on it, eventually returning something else. Of course this can also be accomplished by using let. But that's okay. I'm just pointing out the differences.

With

With (fun <T, R> with(receiver: T, block: T.() -> R): R) is a very interesting method. Don't confuse it with Python's with statement, because Kotlin's with has nothing to do with auto-closing objects. Instead it takes a receiver, and executes a block of code on it, returning the result of that block of code. We could write something like this:

              
fun brewBetterBeer(brewery: Brewery) =
    // Make better 🍺, we just add more of each ingredient
    // than all of the others. Except for the water, obviously
    with(brewery) {
        maltAmount = maltAmount * 2
        barleyAmount = barleyAmount * 2
        yeastAmount = yeastAmount * 2
        createMixture().brew()
    }
            

So as you can see this works very well when you want to do something with an Object you receive as an argument, and that you need to transform into something else

Conclusion

The Kotlin standard library contains a few closely related slightly distinct functions. Knowing which one to use when will help you mastering Kotlin and get better at choosing the right alternative. The functions let, also, apply and run can also be called in a null-safe way when chaining. This way you are sure to receive a non-null object. For example: possibleNull?.let{ transform(it) }?.also { cacheIt(it) } etc.
I hope you enjoyed this post.

Cheers! 🍻