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
- Featured image by ThisIsEngineering from Pexels