Exploring the Magic of Variable Capturing in Kotlin Lambdas: A Hands-On Approach

Table of Contents

Kotlin is a modern, statically typed programming language that runs on the Java Virtual Machine (JVM). One of its key features is support for lambda expressions, which provide a concise and expressive way to define functions inline. In Kotlin, lambdas can capture local variables, which allows them to extend the scope of those variables beyond the function in which they are declared. This feature is extremely powerful, but it can also be somewhat confusing if you’re not familiar with how variable capturing works. In this article, we’ll explore the topic of variable capturing in Kotlin lambdas in-depth, with plenty of examples along the way.

What is Variable Capturing?

In Kotlin, the lifetime of a local variable is determined by the function in which it is declared. This means that the variable can only be accessed within that function and will be destroyed once the function finishes executing.

However, if a local variable is captured by a lambda expression, the variable’s scope can be extended beyond the function in which it was declared. This means that the code that uses the variable can be stored and executed later.

If the variable is declared as final, its value is stored together with the lambda code that uses it. This is because the value of a final variable cannot be changed once it has been assigned.

On the other hand, if the variable is not final, its value is enclosed in a special wrapper that allows you to change it. The reference to this wrapper is then stored together with the lambda, so that the lambda can access and modify the value of the variable even after the function in which it was declared has finished executing.

This behavior is called “capturing” a variable, and it is a powerful feature of Kotlin’s lambda expressions that allows for more flexible and expressive programming.

Examples

Let’s dive into some code examples to better understand how local variables are captured by lambdas in Kotlin.

First, let’s define a simple function that takes an integer argument and returns a lambda that multiplies its input by a factor:

Kotlin
fun multiplyBy(factor: Int): (Int) -> Int {
    return { input: Int -> input * factor }
}

In this example, the function multiplyBy returns a lambda that captures the factor variable. When the lambda is executed, it multiplies its input parameter by factor and returns the result.

We can use this function to create two lambdas that multiply their input by different factors:

Kotlin
val double = multiplyBy(2)
val triple = multiplyBy(3)

Here, we’re creating two new lambdas by calling multiplyBy with different values for factor. double captures the value 2, while triple captures the value 3.

Now, we can use these lambdas to perform some calculations:

Kotlin
val result1 = double(5)   // returns 10
val result2 = triple(5)  // returns 15

Here, we’re calling double and triple with the input value 5. double(5) returns 10, because 2 * 5 = 10. triple(5) returns 15, because 3 * 5 = 15.

Notice that even though double and triple capture the value of factor when they are created, they can be executed with different input values later. This is because the captured factor variable is stored along with the lambda code, and can be used each time the lambda is executed.

Now, let’s look at an example of capturing a non-final variable. Consider the following function:

Kotlin
fun counter(): () -> Int {
    var count = 0
    return {
        count++
        count
    }
}

This function returns a lambda that increments and returns a local variable count each time it is executed. The count variable is not declared as final, which means that its value can be changed.

We can use this function to create two lambdas that count the number of times they are executed:

Kotlin
val increment1 = counter()
val increment2 = counter()

Here, we’re creating two new lambdas by calling counter twice. increment1 and increment2 both capture the same count variable.

Now, let’s execute these lambdas and see what happens:

Kotlin
val result1 = increment1()   // returns 1
val result2 = increment2()  // returns 1
val result3 = increment1()   // returns 2
val result4 = increment2()  // returns 2

Here, we’re calling increment1 and increment2 multiple times. The first time each lambda is called, it returns 1, because the initial value of count is 0. The second time each lambda is called, it returns 2, because the value of count has been incremented once.

Notice that both increment1 and increment2 are accessing the same count variable, and that the value of count is being modified each time the lambdas are executed. This is possible because Kotlin creates a special wrapper object for non-final captured variables that allows their values to be modified by the lambda.

Final Variables

When a lambda captures a local variable in Kotlin, the captured variable must be either val or final in Java terminology. This means that the variable must be immutable, or effectively immutable, by the time the lambda captures it. If the variable is mutable, the behavior of the lambda can be unpredictable.

Here’s an example that demonstrates capturing a final variable in Kotlin:

Kotlin
fun outerFunction(): () -> Unit {
    val message = "Hello, softAai!"
    return { println(message) }
}

val lambda = outerFunction()
lambda() // prints "Hello, softAai!"

In this example, the outerFunction returns a lambda that captures the final variable message. The lambda prints the value of message when it’s executed. The value of message cannot be modified, so this lambda will always print “Hello, softAai!”.

Non-Final Variables

When a lambda captures a non-final variable in Kotlin, the variable is effectively wrapped in an object that can be modified by the lambda. This allows the lambda to modify the value of the variable even after it has been captured. However, there are some important rules to keep in mind when capturing non-final variables.

Here’s an example that demonstrates capturing a non-final variable in Kotlin:

Kotlin
fun outerFunction(): () -> Unit {
    var counter = 0
    return { println(counter++) }
}

val lambda = outerFunction()
lambda() // prints "0"
lambda() // prints "1"

In this example, the outerFunction returns a lambda that captures the non-final variable counter. The lambda prints the value of counter when it’s executed and increments it by one. The value of counter can be modified by the lambda, so each time the lambda is executed, the value of counter will increase.

However, if you try to modify a captured variable from outside the lambda, you’ll get a compilation error:

Kotlin
fun outerFunction(): () -> Unit {
    var counter = 0
    return { println(counter++) }
}

val lambda = outerFunction()
lambda.counter = 10 // Compilation error: "Unresolved reference: counter"

This is because the captured variable is effectively wrapped in an object that can only be accessed and modified by the lambda itself. If you want to modify the value of the captured variable from outside the lambda, you’ll need to create a separate variable and update it manually:

Kotlin
fun outerFunction(): () -> Unit {
    var counter = 0
    return {
        val newCounter = 10
        println(newCounter)
    }
}

val lambda = outerFunction()
lambda() // prints "10"

In this example, we’ve created a new variable called newCounter inside the lambda and assigned it a value of 10. This allows us to modify the value of the variable without modifying the captured variable.

Capturing mutable variables

The concept of capturing mutable variables in Kotlin lambdas may be a bit confusing, especially for those coming from Java, where only final variables can be captured.

In Kotlin, you can use a trick to capture mutable variables by either declaring an array of one element in which to store the mutable value, or by creating an instance of a wrapper class that stores the reference that can be changed.

To illustrate this, you can create a Ref class with a mutable value property, which can be used to capture a mutable variable in a lambda. Here’s an example of how this can be done:

Kotlin
class Ref<T>(var value: T)

val counter = Ref(0)
val inc = { counter.value++ }

In this example, a counter variable of type Ref is created with an initial value of 0, and a lambda expression inc is defined to increment the value property of the counter object each time it’s called.

By using the Ref class, you are simulating the capturing of a mutable variable in a lambda, by actually capturing an immutable reference to an instance of the Ref class, which can be mutated to change the value of its value property.

So in Kotlin, you can directly capture a mutable variable like a var by simply referencing it within the lambda. This is because, under the hood, Kotlin creates an instance of a Ref class to capture the mutable variable, and any changes made to it are reflected in the original variable outside the lambda.

Here’s an example:

Kotlin
var counter = 0
val inc = { counter++ }

In this example, a counter variable is declared as a var with an initial value of 0, and a lambda expression inc is defined to increment the counter variable each time it’s called.

As here mentioned, the first example with the Ref class shows how the second example works under the hood. When you capture a final variable (val), its value is copied, similar to how it works in Java. However, when you capture a mutable variable (var), Kotlin creates an instance of a Ref class to store the value of the mutable variable, which is then captured as a final variable. The actual value of the mutable variable is then stored in a field of the Ref class, which can be changed from the lambda.

Capturing Objects

When a lambda captures an object in Kotlin, it captures a reference to the object rather than a copy of the object itself. This means that if you modify the object outside the lambda, the changes will be visible inside the lambda.

Here’s an example that demonstrates capturing an object in Kotlin:

Kotlin
class Counter {
    var value = 0
}

fun outerFunction(): () -> Unit {
    val counter = Counter()
    return { println(counter.value++) }
}

val lambda = outerFunction()
lambda() // prints "0"
lambda() // prints "1"

In this example, we’ve defined a simple Counter class with a single value property. We’ve also defined an outerFunction that creates a new Counter object and returns a lambda that captures the object. The lambda prints the value of the value property when it’s executed and increments it by one.

If you modify the value property of the Counter object outside the lambda, the changes will be visible inside the lambda:

Kotlin
class Counter {
    var value = 0
}

fun outerFunction(): () -> Unit {
    val counter = Counter()
    return { println(counter.value++) }
}

val lambda = outerFunction()
lambda() // prints "0"
lambda() // prints "1"
lambda() // prints "2"
lambda() // prints "3"

val counter = Counter()
counter.value = 10
lambda() // prints "4"

In this example, we’ve created a new Counter object called counter and set its value property to 10. When we call the lambda again, it prints “4”, which shows that the changes to the Counter object are visible inside the lambda as here we deal with another object so changes won’t reflect.

Let’s take another example to understand it clearly.

Kotlin
data class Person(val name: String, var age: Int)

fun main() {
    var person = Person("Alice", 30)

    val incrementAge = { person.age += 1 }

    println(person) // Output: Person(name=Alice, age=30)

    incrementAge()

    println(person) // Output: Person(name=Alice, age=31)

    person.age += 1

    println(person) // Output: Person(name=Alice, age=32)

    incrementAge()

    println(person) // Output: Person(name=Alice, age=33)
}

In this example, we have a Person class with a name and an age property. We also have a lambda expression incrementAge that captures the person object and increments its age property by 1.

When we execute the program, we first print the person object, which has an age of 30. We then execute the incrementAge lambda expression, which modifies the age property of the person object to 31. We print the person object again and see that its age property has been updated to 31.

After that, we modify the age property of the person object outside of the lambda expression, by incrementing it by 1. We print the person object again and see that its age property has been updated to 32.

Finally, we execute the incrementAge lambda expression again, which modifies the age property of the person object to 33. We print the person object one last time and see that its age property has been updated to 33.

What’s happening here is that when we define the incrementAge lambda expression, it captures a reference to the person object, not a copy of it. This means that when we execute the lambda expression and modify the age property of the person object, we are modifying the same object that exists outside of the lambda expression.

So, when we modify the age property of the person object outside of the lambda expression, those changes are visible inside the lambda expression because they are happening to the same object that the lambda expression has captured a reference to.

Conclusion

Capturing variables and objects in Kotlin lambdas can be a powerful tool for writing concise and expressive code. By understanding the rules for capturing final and non-final variables and objects, you can write code that behaves exactly as you expect. However, it’s important to be careful when capturing variables and objects, especially when working with mutable state. By following these guidelines, you can write safe and effective Kotlin code that uses lambdas to their full potential.

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!