Hello Readers, CoolMonkTechie heartily welcomes you in this article (How To Select Scope Functions In Kotlin?).
In this article, we will learn about how to select Scope Functions in Kotlin. The Kotlin standard library contains several functions whose sole purpose is to execute a block of code within the context of an object. When we call such a function on an object with a lambda expression provided, it forms a temporary scope. In this scope, we can access the object without its name. Such functions are called Scope Functions. This article provides the detailed descriptions of the differences between scope functions and the conventions on their usage in Kotlin with some authentic examples.
A famous quote about learning is :
” Tell me and I forget, teach me and I may remember, involve me and I learn.”
So Let’s begin.
Scope Functions in Kotlin
The definition of Scope function is
” Scoped functions are functions that execute a block of code within the context of an object.“
These functions provide a way to give temporary scope to the object under consideration where specific operations can be applied to the object within the block of code, thereby, resulting in a clean and concise code. There are five scoped functions in Kotlin: let, run, with, also and apply.
Basically, these functions do the same: execute a block of code on an object. What’s different is how this object becomes available inside the block and what is the result of the whole expression.
Example
Here’s a typical usage of a scope function:
Person("Alice", 20, "Amsterdam").let {
println(it)
it.moveTo("London")
it.incrementAge()
println(it)
}
If we write the same without let
, we’ll have to introduce a new variable and repeat its name whenever we use it.
val alice = Person("Alice", 20, "Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge()
println(alice)
The scope functions do not introduce any new technical capabilities, but they can make our code more concise and readable.
Due to the similar nature of scope functions, choosing the right one for our case can be a bit tricky. The choice mainly depends on our intent and the consistency of use in our project.
Common Difference between Scope Functions
Because the scope functions are all quite similar in nature, it’s important to understand the differences between them. There are two main differences between each scope function:
- The way to refer to the context object
- The return value.
Context object – this or it
Inside the lambda of a scope function, the context object is available by a short reference instead of its actual name. Each scope function uses one of two ways to access the context object: as a lambda receiver (this
) or as a lambda argument (it
). Both provide the same capabilities, so we’ll describe the pros and cons of each for different cases and provide recommendations on their use.
fun main() {
val str = "Hello"
// this
str.run {
println("The receiver string length: $length")
//println("The receiver string length: ${this.length}") // does the same
}
// it
str.let {
println("The receiver string's length is ${it.length}")
}
}
this
run
, with
, and apply
refer to the context object as a lambda receiver – by keyword this
. Hence, in their lambdas, the object is available as it would be in ordinary class functions. In most cases, we can omit this
when accessing the members of the receiver object, making the code shorter. On the other hand, if this
is omitted, it can be hard to distinguish between the receiver members and external objects or functions. So, having the context object as a receiver (this
) is recommended for lambdas that mainly operate on the object members: call its functions or assign properties.
val adam = Person("Adam").apply {
age = 20 // same as this.age = 20 or adam.age = 20
city = "London"
}
println(adam)
it
In turn, let
and also
have the context object as a lambda argument. If the argument name is not specified, the object is accessed by the implicit default name it
. it
is shorter than this
and expressions with it
are usually easier for reading. However, when calling the object functions or properties we don’t have the object available implicitly like this
. Hence, having the context object as it
is better when the object is mostly used as an argument in function calls. it
is also better if we use multiple variables in the code block.
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}
val i = getRandomInt()
Additionally, when we pass the context object as an argument, we can provide a custom name for the context object inside the scope.
fun getRandomInt(): Int {
return Random.nextInt(100).also { value ->
writeToLog("getRandomInt() generated value $value")
}
}
val i = getRandomInt()
Return value
The scope functions differ by the result they return:
apply
andalso
return the context object.let
,run
, andwith
return the lambda result.
These two options let we choose the proper function depending on what we do next in our code.
Context object
The return value of apply
and also
is the context object itself. Hence, they can be included into call chains as side steps: we can continue chaining function calls on the same object after them.
val numberList = mutableListOf()
numberList.also { println("Populating the list") }
.apply {
add(2.71)
add(3.14)
add(1.0)
}
.also { println("Sorting the list") }
.sort()
They also can be used in return statements of functions returning the context object.
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}
val i = getRandomInt()
Lambda result
let
, run
, and with
return the lambda result. So, we can use them when assigning the result to a variable, chaining operations on the result, and so on.
val numbers = mutableListOf("one", "two", "three")
val countEndsWithE = numbers.run {
add("four")
add("five")
count { it.endsWith("e") }
}
println("There are $countEndsWithE elements that end with e.")
Additionally, we can ignore the return value and use a scope function to create a temporary scope for variables.
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
val firstItem = first()
val lastItem = last()
println("First item: $firstItem, last item: $lastItem")
}
We can analyze the common scope functions difference summary as below diagram:
Five Scope Functions In Kotlin
To help we choose the right scope function for our case, we’ll describe them in detail and provide usage recommendations. Technically, functions are interchangeable in many cases, so the examples show the conventions that define the common usage style.
There are five scoped functions in Kotlin: let, run, with, also and apply.
Let’s go through them one by one.
Scope Function – let
The context object is available as an argument (it
). The return value is the lambda result.
let
can be used to invoke one or more functions on results of call chains. For example, the following code prints the results of two operations on a collection:
val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)
With let
, we can rewrite it:
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let {
println(it)
// and more function calls if needed
}
If the code block contains a single function with it
as an argument, we can use the method reference (::
) instead of the lambda:
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println)
let
is often used for executing a code block only with non-null values. To perform actions on a non-null object, use the safe call operator ?.
on it and call let
with the actions in its lambda.
val str: String? = "Hello"
//processNonNullString(str) // compilation error: str can be null
val length = str?.let {
println("let() called on $it")
processNonNullString(it) // OK: 'it' is not null inside '?.let { }'
it.length
}
Another case for using let
is introducing local variables with a limited scope for improving code readability. To define a new variable for the context object, provide its name as the lambda argument so that it can be used instead of the default it
.
val numbers = listOf("one", "two", "three", "four")
val modifiedFirstItem = numbers.first().let { firstItem ->
println("The first item of the list is '$firstItem'")
if (firstItem.length >= 5) firstItem else "!" + firstItem + "!"
}.toUpperCase()
println("First item after modifications: '$modifiedFirstItem'")
Scope Function – with
A non-extension function: the context object is passed as an argument, but inside the lambda, it’s available as a receiver (this
). The return value is the lambda result.
We recommend with
for calling functions on the context object without providing the lambda result. In the code, with
can be read as “with this object, do the following.”
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' is called with argument $this")
println("It contains $size elements")
}
Another use case for with
is introducing a helper object whose properties or functions will be used for calculating a value.
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
"The first element is ${first()}," +
" the last element is ${last()}"
}
println(firstAndLast)
It is convenient when we have to call multiple different methods on the same object. Instead of repeating the variable containing this object on each line, we can use with. This function is used to change instance properties without the need to call dot operator over the reference every time.
Scope Function – run
The context object is available as a receiver (this
). The return value is the lambda result.
run
does the same as with
but invokes as let
– as an extension function of the context object.
run
is useful when our lambda contains both the object initialization and the computation of the return value.
val service = MultiportService("https://example.kotlinlang.org", 80)
val result = service.run {
port = 8080
query(prepareRequest() + " to port $port")
}
// the same code written with let() function:
val letResult = service.let {
it.port = 8080
it.query(it.prepareRequest() + " to port ${it.port}")
}
Besides calling run
on a receiver object, we can use it as a non-extension function. Non-extension run
lets us execute a block of several statements where an expression is required.
val hexNumberRegex = run {
val digits = "0-9"
val hexDigits = "A-Fa-f"
val sign = "+-"
Regex("[$sign]?[$digits$hexDigits]+")
}
for (match in hexNumberRegex.findAll("+1234 -FFFF not-a-number")) {
println(match.value)
}
run is actually a combination of with() and let().
Scope Function – apply
The context object is available as a receiver (this
). The return value is the object itself.
Use apply
for code blocks that don’t return a value and mainly operate on the members of the receiver object. The common case for apply
is the object configuration. Such calls can be read as “apply the following assignments to the object.”
val adam = Person("Adam").apply {
age = 32
city = "London"
}
println(adam)
Having the receiver as the return value, we can easily include apply
into call chains for more complex processing.
Scope Function – also
The context object is available as an argument (it
). The return value is the object itself.
also
is good for performing some actions that take the context object as an argument. Use also
for actions that need a reference rather to the object than to its properties and functions, or when we don’t want to shadow this
reference from an outer scope.
When we see also
in the code, we can read it as “and also do the following with the object.”
val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("The list elements before adding new one: $it") }
.add("four")
Scope Functions – run vs let
run is similar to let in terms of accepting any return value , but this is different in the context of the object terms. run function refers to the context of the object as “this” and not “it”. That is the reason we did not use “${this.name}” as it would be redundant here since the block of code understands that “name” is used here concerning the Person object.
Another point here is that since the context is referred to as “this”, it cannot be renamed to a readable lambda parameter. So depending on the use case and requirement , we have to choose between the let and the run operator. The “run” operator also helps in easy null checks similar to the “let” operator.
var name: String? = "Abcd"
private fun performRunOperation() {
val name = Person().name?.run {
"The name of the Person is: $this"
}
print(name)
}
Scope Functions – with vs run
Let’s consider a case where a Person object can be nullable.
we can see that the context of the object referred to as “this” is a nullable type of Person. And hence, to correct this, we need to change the code as:
private fun performWithOperation() {
val person: Person? = null
with(person) {
this?.name = "asdf"
this?.contactNumber = "1234"
this?.address = "wasd"
this?.displayInfo()
}
}
So performing a null check using a “with” operator is difficult and this is where we can replace it with “run” as follows:
private fun performRunOperation() {
val person: Person? = null
person?.run {
name = "asdf"
contactNumber = "1234"
address = "wasd"
displayInfo()
}
}
This looks a lot cleaner.
Scope Functions – run vs apply
So let’s see the difference between run and apply functions.
We can see that run accepts a return statement whereas apply does not accept a return statement(we can see the error thrown by the IDE in the image) and always returns the same object which it is referring to.
Scope Functions – let vs also
So let’s see the difference between let and also functions.
We can see that let accepts a return statement whereas “also” does not accept a return statement(we can see the error thrown by the IDE in the image) and always returns the same object which it is referring to.
Standard Library Scope Functions – takeIf and takeUnless
In addition to scope functions, the standard library contains the functions takeIf
and takeUnless
. These functions let us embed checks of the object state in call chains.
When we called on an object with a predicate provided, takeIf
returns this object if it matches the predicate. Otherwise, it returns null
. So, takeIf
is a filtering function for a single object. In turn, takeUnless
returns the object if it doesn’t match the predicate and null
if it does. The object is available as a lambda argument (it
).
val number = Random.nextInt(100)
val evenOrNull = number.takeIf { it % 2 == 0 }
val oddOrNull = number.takeUnless { it % 2 == 0 }
println("even: $evenOrNull, odd: $oddOrNull")
When we do chaining other functions after takeIf
and takeUnless
, we don’t forget to perform the null check or the safe call (?.
) because their return value is nullable.
val str = "Hello"
val caps = str.takeIf { it.isNotEmpty() }?.toUpperCase()
//val caps = str.takeIf { it.isNotEmpty() }.toUpperCase() //compilation error
println(caps)
takeIf
and takeUnless
are especially useful together with scope functions. A good case is chaining them with let
for running a code block on objects that match the given predicate. To do this, call takeIf
on the object and then call let
with a safe call (?
). For objects that don’t match the predicate, takeIf
returns null
and let
isn’t invoked.
fun displaySubstringPosition(input: String, sub: String) {
input.indexOf(sub).takeIf { it >= 0 }?.let {
println("The substring $sub is found in $input.")
println("Its start position is $it.")
}
}
displaySubstringPosition("010000011", "11")
displaySubstringPosition("010000011", "12")
This is how the same function looks without the standard library functions:
fun displaySubstringPosition(input: String, sub: String) {
val index = input.indexOf(sub)
if (index >= 0) {
println("The substring $sub is found in $input.")
println("Its start position is $index.")
}
}
displaySubstringPosition("010000011", "11")
displaySubstringPosition("010000011", "12")
The Selection Of Scope Functions
To help we choose the right scope function for our purpose, we provide the table of key differences between them.
Function | Object reference | Return value | Is extension function |
---|---|---|---|
let | it | Lambda result | Yes |
run | this | Lambda result | Yes |
run | – | Lambda result | No: called without the context object |
with | this | Lambda result | No: takes the context object as an argument. |
apply | this | Context object | Yes |
also | it | Context object | Yes |
Here is a short guidelines for choosing scope functions depending on the intended purpose:
- Executing a lambda on non-null objects:
let
- Introducing an expression as a variable in local scope:
let
- Object configuration:
apply
- Object configuration and computing the result:
run
- Running statements where an expression is required: non-extension
run
- Additional effects:
also
- Grouping function calls on an object:
with
The use cases of different scope functions overlap, so that we can choose the functions based on the specific conventions used in our project or team.
That’s all about in this article.
Related Other Articles / Posts
- Is Awesome Design Patterns Valuable In Kotlin?
- Understanding Kotlin Style Guide
- 5 Quick Valuable Concepts To Learn Kotlin Programming Language
- How To Work Flow APIs In Kotlin ?
- Extension Functions vs Static Utility Class in Kotlin
- Understanding Property Delegation In Kotlin
Conclusion
In this article, we understood about how to select Scope Functions in Kotlin. Although the scope functions are a way of making the code more concise, avoid overusing them: it can decrease our code readability and lead to errors. Avoid nesting scope functions and be careful when chaining them: it’s easy to get confused about the current context object and the value of this
or it
. This article showed the detailed descriptions of the differences between scope functions and the conventions on their usage in Kotlin with some authentic examples.
Thanks for reading! I hope you enjoyed and learned about Scope Functions concepts in Kotlin. Reading is one thing, but the only way to master it is to do it yourself.
Please follow and subscribe to the blog and support us in any way possible. Also like and share the article with others for spread valuable knowledge.
You can find other articles of CoolMonkTechie as below link :
You can also follow official website and tutorials of Android as below links :
If you have any comments, questions, or think I missed something, leave them below in the comment box.
Thanks again Reading. HAPPY READING !!???
I’m truly enjoying the design and layout of your blog.
It’s a very easy on the eyes which makes it much more enjoyable for me to
come here and visit more often. Did you hire out a designer to create your theme?
Fantastic work!
Thank you so much, Your opinion really matters to me, so that’s really nice to hear. Currently, I manage my website and blogs.
Wonderful, what a weblog it is! This webpage presents valuable facts to us, keep it up. Ivan Settlemire
Really enjoyed this post. Really thank you! Really Great. Gregorio Paulman