1. Overview

In Kotlin, let() is a pretty convenient scope function. It allows us to transform the given variable to a value in another type.

In this tutorial, we’ll explore how to apply let-like operations on multiple variables.

2. Introduction to the Problem

First of all, let’s look at a simple let example:

val str:String? = "hello"
val lengthReport = str?.let{"The length of the string [$it] is: ${it.length}"}
println(lengthReport)
//will print: The length of the string [hello] is: 5

In the example above, the let() function produces the length report of the given string. The code is pretty straightforward. However, it’s worth mentioning that the str variable is a nullable String type (String?). Further, we want to call the let() function only if the str variable is not null. Therefore, we’ve used a null-safe let() call: str?.let{ … }. This is also a common technique when we handle nullable types.

Sometimes, we’d like to apply the null-safe *let-*like operation to more than one variable. But the standard let() function can only handle a single variable.

In this tutorial, we’ll first look at the “two variables’ let” case. Then we’ll see if we can build a function to execute the let-like operation on even more variables.

For simplicity, we’ll use unit-test assertions to verify if our functions work as expected.

Next, let’s see them in action.

3. Null-Safe let on Two Variables

Now, we’ll address several approaches to achieving null-safe let calls on two variables.

3.1. Nesting Two let Calls

The most straightforward way to make null-safe let() handle two nullable variables would be to write two let() calls. Next, let’s understand it with an example:

val theName: String? = "Kai"
val theNumber: Int? = 7
val result = theName?.let { name ->
    theNumber?.let { num -> "Hi $name, $num squared is ${num * num}" }
}
assertThat(result).isEqualTo("Hi Kai, 7 squared is 49")

As the code above shows, we have two nested null-safe let() calls. If we run the test, it passes. So, it works as expected.

However, the nested structure isn’t easy to read. So, next, let’s see if we can find a better way to achieve it.

3.2. Creating Our Own let2() Function

A usual way to save some typing and make the code easier to understand is wrapping the logic in a function or method.

Now, let’s create a function to simulate let() so that we can execute lambda expressions on two variables:

inline fun <T1 : Any, T2 : Any, R : Any> let2(p1: T1?, p2: T2?, block: (T1, T2) -> R?): R? {
    return if (p1 != null && p2 != null) block(p1, p2) else null
}

Next, let’s walk through the function implementation quickly and understand what it does.

The function let2() has three parameters: p1, p2, and block. p1 and p2 are the two nullable parameters. block is a function that accepts two not-null arguments and returns a nullable R instance.

As we can see in the code above, let2() is a generic function, so the two parameters’ (p1 and p2) types can be different. The function block decides the return type of the let2() function.

We execute the block(p1, p2) function only if both p1 and p2 are not null. Otherwise, let2() will return null.

In Kotlin, if the last parameter of a function is a function, we can pass a lambda expression to it and put it outside of  (…). For example, we can call our let2() in this way:

let2(v1, v2) { a, b ->  ... (lambda) }

Moreover**, we declare the let2() function as an inline function to gain better performance.**

Next, let’s create a test to see if let2() works as expected. First, let’s declare a nullable Int type (Int?) to hold the null value so that Kotlin knows the concrete type of the null value:

val nullNum: Int? = null

Then, let’s pass a string and an integer to the message to test the let2() function:

assertThat(let2("Kai", 7) { name, num -> "Hi $name, $num squared is ${num * num}" }).isEqualTo("Hi Kai, 7 squared is 49")

assertThat(let2(nullNum, 7) { name, num -> "Hi $name, $num squared is ${num * num}" }).isNull()
assertThat(let2(7, nullNum) { name, num -> "Hi $name, $num squared is ${num * num}" }).isNull()
assertThat(let2(nullNum, nullNum) { name, num -> "Hi $name, $num squared is ${num * num}" }).isNull()

If we execute the test, it passes. So our let2() function can perform the null-safe let-like operation on two nullable variables.

Next, let’s see if we can perform let() on more than two variables.

4. Null-Safe let on Multiple Variables

For simplicity, we’ll address how to execute the *let-*like operation on three variables in this section.

4.1. Extending the let2() Function

As the let2() function works as expected, we can easily extend it to accept three variables. So it won’t be a challenge for us:

inline fun <T1 : Any, T2 : Any, T3 : Any, R : Any> let3(p1: T1?, p2: T2?, p3: T3?, block: (T1, T2, T3) -> R?): R? {
    return if (p1 != null && p2 != null && p3 != null) block(p1, p2, p3) else null
}

Basically, we just extend let2() to let3() by adding a new parameter p3 to the function.

Next, let’s test our let3() function. This time, we’ll use three nullable integers (Int?) variables for simplicity:

assertThat(let3(5, 6, 7) { n1, n2, n3 -> "$n1 + $n2 + $n3 is ${n1 + n2 + n3}" }).isEqualTo("5 + 6 + 7 is 18")
                                                                                                             
assertThat(let3(nullNum, 7, 6) { n1, n2, n3 -> "$n1 + $n2 + $n3 is ${n1 + n2 + n3}" }).isNull()
assertThat(let3(nullNum, nullNum, 6) { n1, n2, n3 -> "$n1 + $n2 + $n3 is ${n1 + n2 + n3}" }).isNull()
assertThat(let3(nullNum, nullNum, nullNum) { n1, n2, n3 -> "$n1 + $n2 + $n3 is ${n1 + n2 + n3}" }).isNull()

Unsurprisingly, the test passes if we execute it. So the let3() function works. However, if we review the functions we’ve created for both let2() and let3(), the number of the parameters is always fixed. Of course, if it’s required, we could still create let4(), let5(), and so on to support even more variables. But it would be good to have one single function to handle a variable number of parameters.

So next, let’s see how to achieve that.

4.2. Handling a Dynamic Number of Variables

In Kotlin, the vararg parameter allows us to pass a dynamic number of arguments to a function. Therefore, we can create a new inline function with a vararg parameter:

inline fun <T : Any, R : Any> letIfAllNotNull(vararg arguments: T?, block: (List<T>) -> R): R? {
    return if (arguments.all { it != null }) {
        block(arguments.filterNotNull())
    } else null
}

As we can see in the letIfAllNotNull() function above, the vararg parameter arguments can have various number of parameters. The block function now accepts a list of elements with the not-null T type.

Similar to the let2() and the let3() functions, we first check if all parameters are not-null values using arguments.all { it != null }. Then, arguments.filterNotNull() converts the nullable vararg parameter (array) into a List of not-null values.

Next, let’s create a test to see how to call this function and verify if it works as expected:

assertThat(letIfAllNotNull(5, 6, 7, 8) { "${it.joinToString(separator = " + ") { num -> "$num" }} is ${it.sum()}" }).isEqualTo("5 + 6 + 7 + 8 is 26")
assertThat(letIfAllNotNull(5, 6, 7) { "${it.joinToString(separator = " + ") { num -> "$num" }} is ${it.sum()}" }).isEqualTo("5 + 6 + 7 is 18")
assertThat(letIfAllNotNull(5, 7) { "${it.joinToString(separator = " + ") { num -> "$num" }} is ${it.sum()}" }).isEqualTo("5 + 7 is 12")
                                                                                                                                                     
assertThat(letIfAllNotNull(nullNum, 7, 6) { "${it.joinToString(separator = " + ") { num -> "$num" }} is ${it.sum()}" }).isNull()
assertThat(letIfAllNotNull(nullNum, null, 6) { "${it.joinToString(separator = " + ") { num -> "$num" }} is ${it.sum()}" }).isNull()
assertThat(letIfAllNotNull(nullNum, nullNum, nullNum) { "${it.joinToString(separator = " + ") { num -> "$num" }} is ${it.sum()}" }).isNull()

As we’ve seen in the test code, since the block() function accepts a list object as the parameter when we write the lambda expression, the it variable is a list containing all variables we’ve passed to letIfAllNotNull().

If we run the test, it’ll pass. So the letIfAllNotNull() function allows us to perform null-safe let-like operations on a dynamic number of nullable variables.

4.3. let on Not-Null Variables

So far, we’ve created functions to apply the null-safe let-like operation on multiple variables. A common requirement among the solutions is that the block function gets called only if all variables are not null.

However, in practice, we may want first to filter out the null values and pass all not-null variables to our block function or the lambda expression.

Finally, let’s make some changes based on the letIfAllNotNull() function to create the letIfAnyNotNull() function:

inline fun <T : Any, R : Any> letIfAnyNotNull(vararg arguments: T?, block: (List<T>) -> R?): R? {
    return if (arguments.any { it != null }) {
        block(arguments.filterNotNull())
    } else null
}

As the code above shows, we check arguments.any { it != null } instead of arguments.all { it != null } so that the block() function gets called if arguments contain any not-null variables.

Next, let’s create a test and some input data to see if letIfAnyNotNull() can produce the expected result:

assertThat(letIfAnyNotNull(5, 6, 7) { "${it.joinToString(separator = " + ") { num -> "$num" }} is ${it.sum()}" }).isEqualTo("5 + 6 + 7 is 18")
assertThat(letIfAnyNotNull(nullNum, 6, 7) { "${it.joinToString(separator = " + ") { num -> "$num" }} is ${it.sum()}" }).isEqualTo("6 + 7 is 13")
assertThat(letIfAnyNotNull(nullNum, nullNum, 7) { "${it.joinToString(separator = " + ") { num -> "$num" }} is ${it.sum()}" }).isEqualTo("7 is 7")
assertThat(letIfAnyNotNull(nullNum, nullNum, nullNum) { "${it.joinToString(separator = " + ") { num -> "$num" }} is ${it.sum()}" }).isNull()

As the assertions show, the letIfAnyNotNull() function will pack all not-null values in a list and pass the list to our lambda expression.

The test passes if we give it a run.

5. Conclusion

In this article, we’ve learned different approaches to performing null-safe *let-*like operations on multiple variables in Kotlin.

As always, the complete source code used in the article can be found over on GitHub.