Introduction
We have already figured out what scope functions are and how apply and also work. In this topic, we will consider three other functions: with, let, and run. They return the result of the last expression and make code more readable and concise.
with
Let's start from with — it's the simplest function among the three. Here are the main characteristics of the with function:
Context object is available as
this.It returns the result of a lambda.
It isn't an extension function.
What do we mean when we say that with isn't an extension function? It means that the context object is passed as an argument — it is enclosed in parentheses. However, inside the lambda our object is available as a receiver (this).
with is used in two cases:
First, when we want to do something with the context object and don't want to receive a result. Remember — with returns the result of a lambda, but according to Kotlin code conventions, we use this scope function when we don't need a certain result. Truly, "What Happens in with, stays in with".
val musicians = mutableListOf("Thom York", "Jonny Greenwood", "Colin Greenwood")
with(musicians) {
println("'with' is called with the argument $this")
println("List contains $size elements")
} // We print the needed data and don't try to get a certain result
Besides, we use with when we want to make an accessory object whose parameters or functions may be used to calculate the result. It is important — this new object is used as an accessory one (we will work with the real object in run).
val musicians = mutableListOf("Thom York", "Jonny Greenwood", "Colin Greenwood")
val firstAndLast = with(musicians) {
"First list element - ${first()}," +
" last list element - ${last()}"
}
println(firstAndLast) // We create a new variable firstAndLast and pass the result of calculations inside the function body to it. After that we print this variable.
When we use with, it sounds like: "Ok, let's do some work with a certain object". Note that with is written to the left of the object itself — it is the only scope function with such syntax.
let
Here are the main features of the let function:
Context object is available as
it.It returns the result of a lambda.
let is used in two general cases:
First, when we want to do something with the safety call operator ? and non-null objects — yes, let allows us to do that. Let's see: in the code below, we try to perform some operations with a nullable string (String?). If we use the standard method, the compiler throws an error. To avoid that, we can check if str is null or non-null when let is called. And do remember that let returns the result of a lambda, which is it.length in our case — the last lambda expression (the last line in the lambda's body).
val str: String? = "Jonny Greenwood"
//processNonNullString(str) // compilation error: str can be null
val length = str?.let {
println("let() is called on $it")
processNonNullString(it) // OK: 'it' is not null inside '?.let { }'
it.length
}
Second, we use let when we want to enter local variables with a limited scope. In such a case, let allows us to improve code readability. Let's see the code below: we don't need to change the first element of musicians, but we can work with it as if with a limited scope element with the full name firstItem (in most cases, we use it or this).
val musicians = listOf("Thom York", "Jonny Greenwood", "Colin Greenwood")
val modifiedFirstItem = musicians.first().let { firstItem ->
println("The first item of the list is '$firstItem'")
if (firstItem.length >= 5) firstItem else "!" + firstItem + "!"
}.uppercase()
println("First item after modifications: '$modifiedFirstItem'")
You can say: "Hey, I can do that by using with". And technically, yes, you can. But wait till we figure out how run works.
run
Now, look at the characteristics of the run function:
Context object is available as
this.It returns the result of a lambda.
run is like with, but it is an extension function. Thus, run does the same thing as with but is invoked like let.
When can we use run? Mostly in two cases:
First, when we want to initialize a new object and pass the result of a lambda to it. It is important — our new object is independent and valuable, unlike in the case of the with function. For instance, in the code below, we create a new object result, pass a new value to the service element port, and pass to result the result of the query() function with the prepareRequest() function concatenated with a string as a parameter. Note! The value of service.port is changed.
class MultiportService(var url: String, var port: Int) {
fun prepareRequest(): String = "Default request"
fun query(request: String): String = "Result for query '$request'"
}
fun main() {
val service = MultiportService("https://example.kotlinlang.org", 80)
val result = service.run {
port = 8080
query(prepareRequest() + " to port $port")
}
}
Second, when we want to use a function without an extension and execute a block of several operators. In that case, we don't use a context object and just organize some piece of code related to the variable hexNumberRegex.
fun main() {
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)
}
}
We've been twice surprised that these functions can be used interchangeably. Yes, they really are, and we'll unravel this confusion in the following topic. But right now, you can look at the official documentation.
Conclusion
So, we've figured out how to work with three scope functions, which return lambda calculations.
withis a non-extension function and is used for grouping function calls.letusually helps us to work with the safety call operator?or introduce an expression as a variable in a local scope.runis used when we want to configure our object or configure it and return a certain result.
Now, time for some practice.