DEV Community

Sagar
Sagar

Posted on

Kotlin: Scope functions

Prerequisite/Previous articles:

  1. Kotlin: Function type, higher order function, function literal, lambda expression and an anonymous function
  2. Kotlin: Extension function and Receiver type
  3. Kotlin: Inline function

In a hurry?

You can jump to the conclusion section and play the concept.

Recap

  • We can access the receiver type from the business logic of an extension function through the keyword: this
  • We can omit the keyword: this
  • If a lambda has only one parameter, we can access that argument using the keyword: it

Why scope functions?

To make the code more concise!

How does the scope function make the code more concise?

Let us understand it by an example:

As you can see, we could avoid an extra variable. Moreover, we can also omit this keyword as below:

That was neat and clean. Isn't it?

Let us understand our first scope function: with

With

Definition

public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    /*...*/
    return receiver.block()
}

Let us examine the definition.

Inline

We use the inline keyword for the higher order function to avoid a function object creation, memory allocation and runtime overhead.

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

  1. It is a generic function.
  2. It can have anything as the first parameter. We can pass any instance as the first argument.
  3. Second parameter is a function type, named as block.
  4. block is an extension function.
  5. The receiver type of the block is whatever we pass as the first argument to the scope function: with.
  6. We can access the receiver type T from extension function T.() -> R using the keyword: this.
  7. The function type parameter is the last parameter in the higher order function with. That means, we can pass our function literal between curly braces as an argument after the with function call parentheses.
  8. The return type of both the function type parameter and the scope function itself are same: R.
  9. The return type R is generic. That means, whatever our function literal (that we pass as an argument to our with function) returns, will be the return of our with function.
  10. In function literals, the last statement gives return.

So, using with, we can perform multiple operations on an object without explicitly writing the name of the object or without taking another variable.

However, If the variable is nullable, we cannot avoid that this keyword in with.

Suppose we were getting a nullable value in our last example from below function:

private fun getEvenOrNull(randomNumber: Int): Int? =
   if (randomNumber.rem(2) == 0) randomNumber else null

Handling a nullable variable using with would look like below:

That doesn't look good! There must be a “nullability check” before we perform anything on a “nullable” variable.

Using another scope function called let, we can perform the “nullability check” and it also reduces the code than above and make it more pretty like below:

So, let us examine: let

let

Definition

public inline fun <T, R> T.let(block: (T) -> R): R {
    /*...*/
    return block(this)
}

T.let

We know about an extension function. T represents generics here. So, let is a generic extension function. That means, we can use let for any object. It is a member function for every class.

T.let(block: (T) -> R): R

Facts:

  1. let is a higher order function because it has a function type parameter.
  2. let is also an extension function with a generic receiver type T.
  3. The receiver type T is also a parameter of the function type block here.
  4. block is our function type parameter of the let function. We can pass a function type argument using function literal, lambda expression or an anonymous function.
  5. Because the let function is generic and it has only one parameter which is also a generic, mostly we use function literal to pass as a function type argument.
  6. The function type parameter block is the only parameter of a higher order function let and the function type has also only one parameter (T) which is also the receiver type. So, while calling let, we can omit the function call parentheses, the parameter of the function type and the arrow ->. We will just write our business logic between curly braces and we can access the parameter (T) of the function type block, using the keyword: it.
  7. So, we access the receiver type as well as parameter of the function type inside the let block using the keyword: it.
  8. The return type R is generic. The let function returns whatever our function literal (that we pass as a function type argument for the let function) returns.

Why let?

Because, we don't like that this?. for a "nullable variable" multiple times!

  1. To provide “nullability check”.
  2. To introduce chain operations.

Wait. What? Chain operations? Let us understand what it is.

Suppose we have a normal function like below:

fun doSomething(name: String) {
   println("Name from the method doSomething is: $name")
}

Now, let us do some multiple random operations like below:

Now, let us do the same thing using let:

As you can see, using let, we could avoid new variables while using the return value from multiple chain operations.

What?! Understanding the example:

Let us see what we have done (how we have used the let function):

  1. let is a generic extension function. So, any instance can call it. Here, the let function is called on a nullable int variable.
  2. let is also a higher order function. it has only one parameter. The parameter is of function type. The function type parameter has also only one parameter. Hence, we can omit the function call parentheses, the parameter of the function type and the arrow ->. All we write is just our business logic between curly braces and we access the parameter of the function type (which is also the receiver type of our extension function) using the keyword it.
  3. We have done the same. After the function name which is “let”, there is our business logic between curly braces. The keyword it represents our int instance for the first let block and a StringBuilder instance for the second and third block.

Few things to note here:

  1. The last statement in the let block decides the return type.
  2. We are returning the result and not the receiver type itself.
  3. We can check the nullability of the receiver type before we proceed.
  4. It is an extension function. So, we can chain multiple operations. The return type (last statement) of the previous let block will become the receiver type of next function.
  5. When we want to access the receiver type, we can use the keyword it (We have to use it to access the receiver type).
  6. We can use any function on the receiver type from the let block using the keyword: it.
  7. We can pass the receiver type to another function within the let block using the keyword: it.

With Vs Let

If there is a nullable value or chain operations, use let. Otherwise, use with because we can omit the keyword this.

Can't we have a function that can omit the keyword this like a with function and support nullability check and chain operations like let?

Yes! Our next scope function run is a hybrid product which has power of both let and with.

Let us study: run

Run

Below is the definition of the run function.

public inline fun <T, R> T.run(block: T.() -> R): R {
   /*...*/
   return block()
}

Similar to the let function, run is also an extension function and a higher order function. Hence, it also uses the keyword inline to avoid the function object creation.

The differences between let and run functions are:

  1. The function type parameter
  2. Return type

Let us examine it.

T.run(block: T.() -> R): R

  1. The receiver type of an extension function and its function type is same: T. That means, we can access the receiver type using the keyword: this.
  2. The keyword this can be omitted when we want to call any function on it that reduces the code even more.
  3. It seems that the run function has combined the power of with and let. Or maybe we can say it is a with+ version! With was lacking the pros of an extension function (nullability check and chain operations) which the run function satisfies.

Instead of let, If we use run and rewrite the same function we have written earlier for let, it would look like below:

There are few things common between run and let as below. It will help us in deciding when to use them:

  1. Both are extension functions.
  2. Last statement is the return type.
  3. We can check the nullability before we proceed.
  4. Extension functions are helpful for chain operations.

However, there is a small but important difference between them as below. It will help us in selecting the right scope function.

In let, we access the receiver type using the keyword it and it cannot be omitted. However, in run, we access the receiver type using the keyword this and it can be omitted sometimes.

So, if we are not passing the receiver as an argument, we better use run because it reduces the code.

Considering above facts, we can rewrite our last example in a better way like below:

StringBuilder(toString()) looks more concise than StringBuilder(it.toString()).

Similarly, append(" test") looks more concise than it.append(" test").

However, wouldn’t it be more concise if we can replace all $this by $it?

But how? We have to use this to refer to the context object in run.
Correct, but not for "also”. We can use it to refer to the context object while using also.

Then, how is it different from let?

also” returns the object itself whereas the last statement is “return type” for let.

Ok, enough theory! Show me code!

Also

Definition

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

T.also

It is an extension function. That means, we can use it for “nullability check” before we proceed to work on a nullable value.

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

(block: (T) -> Unit)

block is a function type parameter.

block has one and only parameter (T).

We know that if the function type has one and only argument, we can access the argument using the keyword it.

T , The only parameter of the block is also a receiver type here.

So, we will be accessing the receiver type T using the keyword: it.

The interesting part is, it returns the receiver type itself: T

Using also, We can rewrite our last example code as below:

As shown in the example, also returns the object itself despite anything written in it's block and it uses it to refer to the context object.

So, It uses it to refer to the context object. That means, if we need to call some methods on the context object, we will have to use it like below:

So, we had to use it while calling a function on the context object from also block similar to let.

Can we omit it to make the code even more concise?

Yes! By using apply instead of also!

Similar to run, apply also uses this to refer to the context object. However, unlike run, being a sibling of also, apply returns the context object itself.

Let us examine apply.

Apply

Definition

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

T.apply

It is an extension function. Useful for “nullability check”.

block: T.() -> Unit

block is a function type parameter.

The receiver type of an extension function T.apply and it's function type parameter block is same: T.
And we know that we can refer to a receiver type using the keyword: this
And we also know that we can omit the keyword this while calling a function on it.

T.apply(block: T.() -> Unit): T

As we can see, it returns the receiver type itself.

Let us see how we can include apply in our last example:

Conclusion:

We have scope functions to make the code more concise.
All scope functions can be categorized or differentiated by two facts:

  1. The way to refer to the context object (this or it)
  2. Return type (the object itself or the last statement)

Using these two facts, we can decide which scope function to use for a particular situation.

1) If we need last statement as a return, we have:

  • T.run
  • T.let
  • with

Anything that the with can do, can be done by T.run also. Clearly, T.run wins here because of it's added “nullability check” and “chain operation” capabilities.

T.run is better than T.let if we are not passing the receiver type as an argument to another function.

2) If we want to return the object itself, we have:

  • apply (uses this to refer to the object)
  • also (uses it to refer to the object)

Same as the comparison between T.run Vs T.let; apply is better than also if we are not passing the context object as an argument to another function.

Play an overview of this article:

That’s all! If you find this article helpful, you can click on that heart icon 😉

Applauds and creative critics are always welcome 😇. Your ❤ motivates me to write more articles.

Thanks for reading the article! Have a great day 😇.

Relevant previous articles

Inline function
Extension function and Receiver type
Function type, Function literal, Lambda expression and Anonymous function

You can follow me to get the notification when I publish a new article.

Let us be Connected

https://www.linkedin.com/in/srdpatel

https://twitter.com/iSrdPatel

Tags: kotlin, scopeFunctions, android

Latest comments (0)