Differences between let, also, run, with, and apply in Kotlin

Differences between let, also, run, with, and apply in Kotlin

Kotlin supports different scope functions for many purposes. Some have overlapping functionalities such as let, also, and so on. In this article, we discuss the differences between let, also, run, with, and apply functions in Kotlin.

Introduction

Kotlin is one of the most beloved JVM languages. It is less verbose compared to Java. Additionally, unlike Scale, Kotlin does not have a steep learning curve. Yet, it is powerful and fun to code.

However, the abundance of scope functions and their overlapping functionalities may confuse developers, especially those that recently embarked on the Kotlin journey.

The most confusing ones are the ones that don’t exist in Java. If you are a Java developer, you may be shocked to know keywords like let, also, run, and with exist, and they have very similar functionality with some subtle differences.

It is crucial to learn about the differences so that you make the best of the language and write cleaner code.

What is let

The let scope function takes the variable T and returns a result R.

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

Based on the definition, let returns a result that may not be the same type as the provided type.

val str : String = "test"
val strUpper = str.let { it -> it.toUpperCase() }
assertThat(strUpper).isEqualTo("TEST")

This scope function is commonly used for scoping and null-checking. It is similar to the Java map function.

Null checking example,

val str : String? = null
str?.let {
    println("$it is not null")
}

The print statement will execute if str is not null. Otherwise, nothing happens.

What is also

The also scope function takes a nullable T and returns the same object.

inline fun <T> T.also(block: (T) -> Unit): T

Essentially also does not map anything. It is useful for additional processing or performing side effects on the object. For example,

val str : String = "test"
val strUpper = str.also { it -> it.toUpperCase() }
assertThat(strUpper).isEqualTo("test")

A side effect example,

val order = Order(10)
order.also { it.addQuantity(20) }
assertThat(order.quantity).isEqualTo(30)

class Order(val quantity: Int) {
    fun addQuantity(quantity: Int) {
        this.quantity += quantity
    }
}

What is run

The run scope function has two utilities.

The first form is to specify the this value as a receiver and returns a result that may or may not differ from the this type.

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

The second form is to pass a function block and get the result,

inline fun <R> run(block: () -> R): R

For example,

val str : String = "test"
val anotherStr: String = "another_test"

// first form
val strUpper = str.run { this.toUpperCase() }
assertThat(strUpper).isEqualTo("TEST")

// second form
val anotherStrUpper = str.run { anotherStr.toUpperCase() }
assertThat(anotherStrUpper).isEqualTo("ANOTHER_TEST")

This scope function is very similar to let. However, it works on both nullable and non-nullable objects. It is ideal for object initialization inside of the block.

What is with

The with function takes two parameters. The receiver T is a lambda expression and results in R that may or may not be the same as T.

inline fun <T, R> with(receiver: T, block: T.() -> R): R

For example,

val str : String = "test"
val strUpper = with(str) {
    toUpperCase()
}
assertThat(strUpper).isEqualTo("TEST")

However, the primary utility of with is to access an object’s members without the dot notation.

val order = Order(10)

with(order) {
    println(quantity)
    addQuantity(20)
}

assertThat(order.quantity).isEqualTo(30)

class Order(val quantity: Int) {
    fun addQuantity(quantity: Int) {
        this.quantity += quantity
    }
}

What is apply

The apply function accepts a lambda expression, applies it to the object T, and returns self.

inline fun <T> T.apply(block: T.() -> Unit): T

For example,

val str = "test"
val strUpper = str.apply { this.toUpperCase() }
assertThat(strUpper).isEqualTo("test")

As you may already have guessed, apply operates similarly to also. However, it is advised to use it only on non-nullable objects.

This function is ideal for setting some object properties or initializing the object in a builder-style pattern. For example,

val person = PersonBuilder().apply {
    name = "John Wick"
    age = 46
    address = "Continental Hotel"
}.build()

println("Person: $person")

class PersonBuilder {
    var name: String = ""
    var age: Int = 0
    var address: String = ""

    fun build(): Person {
        return Person(name, age, address)
    }
}

data class Person(val name: String, val age: Int, val address: String)

Conclusion

In this article, we explored different scope functions and learned the differences between let, also, run, with, and apply in Kotlin. For every scope function, we provided an example and explained when each should be used under which circumstances. For more Kotlin content, check here.

References

Inline/featured images credits