Previously, we've learned what lambda expressions are. Today, we will take a deeper look into them and learn how to use some of their advanced features.
Complex lambdas
Sometimes, the code in a lambda isn't short enough to fit in one line, so you need to split the code into lines. In such a case, the last line inside the lambda is treated as the lambda return value:
fun main() {
val originalText = "I don't know... what to say...123456"
val textWithoutSmallDigits = originalText.filter {
val isNotDigit = !it.isDigit()
val stringRepresentation = it.toString()
isNotDigit || stringRepresentation.toInt() >= 5
}
println(textWithoutSmallDigits)
}
The output will be:
// I don't know... what to say...56
Besides, a lambda can contain earlier returns. In Kotlin, "earlier returns" refer to the ability to terminate the execution of a lambda expression or a function before reaching the end of its block by using the return keyword.
Earlier returns must be written using the qualified return syntax. This means that after the return keyword, we need to write the @ symbol and the label name. The label name is usually the name of the function where the lambda is passed. Let's now rewrite the previous lambda without changing its result:
fun main() {
val originalText = "I don't know... what to say...123456"
val textWithoutSmallDigits = originalText.filter {
if (!it.isDigit()) {
return@filter true
}
it.toString().toInt() >= 5
}
println(textWithoutSmallDigits)
}
In the example above, the lambda expression passed to the filter function uses an earlier return to immediately return true if the current character is not a digit.
It will have the same output:
// I don't know... what to say...56Capturing variables
Capturing variables in a closure, also known as using a captured variable or a captured value, refers to the process of enclosing a variable in a lambda expression or anonymous function so that it can be used inside the function's body.
When you capture a variable in a closure, you're essentially making a copy of the variable and making it available to the function even if the original variable goes out of scope or is no longer accessible. That allows you to write functions that depend on variables from their surrounding environment, without having to pass those variables as arguments to the function.
Captured variables are especially useful in event-driven programming or in situations where you need to define a callback function that depends on some state or context. For example, if you have a function that performs an asynchronous operation, you might want to define a callback function that updates the user interface based on the result of the operation. In such a case, you can capture the user interface elements in the closure and update them from within the callback function.
Now let's discuss the advantage of creating functions at runtime. The point is that all the variables and values that are visible where the lambda is created are visible inside the lambda, too. If a lambda uses a variable that is declared outside the lambda, then we say that the lambda captures the variable.
That works intuitively. In the case of a captured value, the lambda can just read it. If a variable is captured, the lambda and the outside code can change it, and these changes will be visible both in the lambda and in the outside code.
Take a look at the example below:
var count = 0
val changeAndPrint = {
++count
println(count)
}
println(count) // 0
changeAndPrint() // 1
count += 10
changeAndPrint() // 12
println(count) // 12
Here we declare a lambda and assign it to the changeAndPrint variable. The lambda takes the count variable, increments it (increases it by 1), and prints the new value. Take a look at the printed numbers: it's vital to understand that the count variable is available for changes from both inside and outside the lambda.
Here is another example:
fun placeArgument(value: Int, f: (Int, Int) -> Int): (Int) -> Int {
return { i -> f(value, i) }
}
placeArgument transforms the f function, which takes two arguments, into a function that takes a single argument. We achieve that by creating a lambda that takes only one argument and calls the given function with this argument and the given value. Here, the lambda captures value and f.
Take a look at the following code snippet:
fun sum(a: Int, b: Int): Int = a + b
val mul2 = { a: Int, b: Int -> a * b }
We can use that to create other functions, too. Please note that the sum name refers to a function, so we need to receive the object by writing a doubled colon before the name:
val increment = placeArgument(1, ::sum)
val triple = placeArgument(3, mul2)
println(increment(4)) // 5
println(increment(40)) // 41
println(triple(4)) // 12
println(triple(40)) // 120Conclusion
Now we can create functions at runtime. This is convenient when calling functions from the Kotlin standard library, such as processing data, as it helps write less code. Besides, sometimes you need to make custom functional-programming-style functions, and this topic will help you with that, too. Finally, we've seen again that functions are first-class citizens in the Kotlin language.