Mastering Kotlin standard functions: run, with, let, also and apply
Tags: Android App Development, Kotlin, Java, Android, Programming

Some of the Kotlin’s standard functions are so similar that we are not sure which to use. Here I will introduce a simple way to clearly distinguish their differences and how to pick which to use.

Scoping functions

The functions that I’ll focus on are run, with, T.run, T.let, T.also, and T.apply. I call them scoping functions as I view their main function is to provide an inner scope for the caller function.

The simplest way to illustrate scoping is the run function

fun test() {

var mood = "I am sad" run {

val mood = "I am happy"

println(mood) // I am happy

}

println(mood) // I am sad

}

With this, inside the test function, you could have a separate scope where mood is redefined to I am happy before printing, and it is fully enclosed within the run scope.

This scoping function itself seems not very useful. But there’s another nice bit it has more than just the scope; it returns something i.e. the last object within the scope.

Hence the below would be neat, whereby we can apply the show() to both views as below, without calling it twice.

run {

if (firstTimeView) introView else normalView

}.show()

3 attributes of scoping functions

To make scoping functions more interesting, let me categorize their behavior with 3 attributes. I will use these attributes to distinguish them from each other.

1. Normal vs. extension function

If we look at with and T.run , both functions are pretty similar. The below does the same thing.

with(webview.settings) {

javaScriptEnabled = true

databaseEnabled = true

} // similarly webview.settings.run {

javaScriptEnabled = true

databaseEnabled = true

}

However, their difference is one is a normal function i.e. with , while the other is an extension function i.e. T.run .

So the question is, what is the advantage of each?

Imagine if webview.settings could be null, they will look as below.

// Yack!

with(webview.settings) {

this?.javaScriptEnabled = true

this?.databaseEnabled = true

} // Nice.

webview.settings?.run {

javaScriptEnabled = true

databaseEnabled = true

}

In this case, T.run extension function is better, as we could apply to check for nullability before using it.

2. This vs. it argument

If we look at T.run and T.let , both functions are similar except for one thing, the way they accept the argument. The below shows the same logic for both functions.

stringVariable?.run {

println("The length of this String is $length")

} // Similarly. stringVariable?.let {

println("The length of this String is ${it.length}")

}

If you check the T.run function signature, you’ll notice the T.run is just made as extension function calling block: T.() . Hence all within the scope, the T could be referred to as this .In programming, this could be omitted most of the time. Therefore in our example above, we could use $length in the println statement, instead of ${this.length} . I call this as sending in this as an argument.

However, for T.let function signature, you’ll notice that T.let is sending itself into the function i.e. block: (T) . Hence this is like a lambda argument sent it. It could be referred to within the scope function as it . So I call this as sending in it as an argument.

From the above, it does seem like T.run is more superior over T.let as it is more implicit, but there are some subtle advantages of T.let function as below: -

The T.let does provide a clearer distinguish use the given variable function/member vs. the external class function/member

does provide a clearer distinguish use the given variable function/member vs. the external class function/member In the event that this can’t be omitted e.g. when it is sent as a parameter of a function, it is shorter to write than this and clearer.

can’t be omitted e.g. when it is sent as a parameter of a function, is shorter to write than and clearer. The T.let allow better naming of the converted used variable i.e. you could convert it to some other name.

stringVariable?.let {

nonNullString ->

println("The non null string is $nonNullString")

}

3. Return this vs. other types

Now, let’s look at T.let and T.also , both are identical, if we look into the internal function scope of it.

stringVariable?.let {

println("The length of this String is ${it.length}")

} // Exactly the same as below stringVariable?.also {

println("The length of this String is ${it.length}")

}

However their subtle difference is what they return. The T.let returns a different type of value, while T.also returns the T itself i.e. this .

Both are useful for chaining function, whereby T.let let you evolve the operation, and T.also let you perform on the same variable i.e. this .

Simple illustration as below

val original = "abc" // Evolve the value and send to the next chain

original.let {

println("The original String is $it") // "abc"

it.reversed() // evolve it as parameter to send to next let

}.let {

println("The reverse String is $it") // "cba"

it.length // can be evolve to other type

}.let {

println("The length of the String is $it") // 3

} // Wrong

// Same value is sent in the chain (printed answer is wrong)

original.also {

println("The original String is $it") // "abc"

it.reversed() // even if we evolve it, it is useless

}.also {

println("The reverse String is ${it}") // "abc"

it.length // even if we evolve it, it is useless

}.also {

println("The length of the String is ${it}") // "abc"

} // Corrected for also (i.e. manipulate as original string

// Same value is sent in the chain

original.also {

println("The original String is $it") // "abc"

}.also {

println("The reverse String is ${it.reversed()}") // "cba"

}.also {

println("The length of the String is ${it.length}") // 3

}

The T.also may seems meaningless above, as we could easily combine them into a single block of function. Thinking carefully, it has some good advantages

It can provide a very clear separation process on the same objects i.e. making smaller functional sections. It can be very powerful for self-manipulation before being used, making a chaining builder operation.

When both combine the chain, i.e. one evolve itself, one retain itself, it becomes something powerful e.g. below

// Normal approach

fun makeDir(path: String): File {

val result = File(path)

result.mkdirs()

return result

} // Improved approach

fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }

Looking at all attributes

By looking at the 3 attributes, we could pretty much know the function behavior. Let me illustrate the T.apply function, as it has not be mentioned above. The 3 attributes of T.apply is as below…

It is an extension function It send this as it’s argument. It returns this (i.e. itself)

Hence using it, one could imagine, it could be used as

// Normal approach

fun createInstance(args: Bundle) : MyFragment {

val fragment = MyFragment()

fragment.arguments = args

return fragment

} // Improved approach

fun createInstance(args: Bundle)

= MyFragment().apply { arguments = args }

Or we could also making unchained object creation chain-able.

// Normal approach

fun createIntent(intentData: String, intentAction: String): Intent {

val intent = Intent()

intent.action = intentAction

intent.data=Uri.parse(intentData)

return intent

} // Improved approach, chaining

fun createIntent(intentData: String, intentAction: String) =

Intent().apply { action = intentAction }

.apply { data = Uri.parse(intentData) }

Function selections

Hence clearly, with the 3 attributes, we could now categorize the functions accordingly. And based on that, we could form a decision tree below that could help decide what function we want to use pending on what we need.