Multiple variable let in Kotlin

Kotlin

Kotlin Problem Overview


Is there any way to chain multiple lets for multiple nullable variables in kotlin?

fun example(first: String?, second: String?) {
    first?.let {
        second?.let {
            // Do something just if both are != null
        }
    }
}

I mean, something like this:

fun example(first: String?, second: String?) {
    first?.let && second?.let { 
        // Do something just if both are != null
    }
}

Kotlin Solutions


Solution 1 - Kotlin

Here are a few variations, depending on what style you will want to use, if you have everything of same or different types, and if the list unknown number of items...

##Mixed types, all must not be null to calculate a new value

For mixed types you could build a series of functions for each parameter count that may look silly, but work nicely for mixed types:

inline fun <T1: Any, T2: Any, R: Any> safeLet(p1: T1?, p2: T2?, block: (T1, T2)->R?): R? {
    return if (p1 != null && p2 != null) block(p1, p2) else null
}
inline fun <T1: Any, T2: Any, T3: Any, R: Any> safeLet(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
}
inline fun <T1: Any, T2: Any, T3: Any, T4: Any, R: Any> safeLet(p1: T1?, p2: T2?, p3: T3?, p4: T4?, block: (T1, T2, T3, T4)->R?): R? {
    return if (p1 != null && p2 != null && p3 != null && p4 != null) block(p1, p2, p3, p4) else null
}
inline fun <T1: Any, T2: Any, T3: Any, T4: Any, T5: Any, R: Any> safeLet(p1: T1?, p2: T2?, p3: T3?, p4: T4?, p5: T5?, block: (T1, T2, T3, T4, T5)->R?): R? {
    return if (p1 != null && p2 != null && p3 != null && p4 != null && p5 != null) block(p1, p2, p3, p4, p5) else null
}
// ...keep going up to the parameter count you care about

Example usage:

val risk = safeLet(person.name, person.age) { name, age ->
  // do something
}   

##Execute block of code when list has no null items

Two flavours here, first to execute block of code when a list has all non null items, and second to do the same when a list has at least one not null item. Both cases pass a list of non null items to the block of code:

Functions:

fun <T: Any, R: Any> Collection<T?>.whenAllNotNull(block: (List<T>)->R) {
    if (this.all { it != null }) {
        block(this.filterNotNull()) // or do unsafe cast to non null collectino
    }
}

fun <T: Any, R: Any> Collection<T?>.whenAnyNotNull(block: (List<T>)->R) {
    if (this.any { it != null }) {
        block(this.filterNotNull())
    }
}

Example usage:

listOf("something", "else", "matters").whenAllNotNull {
    println(it.joinToString(" "))
} // output "something else matters"

listOf("something", null, "matters").whenAllNotNull {
    println(it.joinToString(" "))
} // no output

listOf("something", null, "matters").whenAnyNotNull {
    println(it.joinToString(" "))
} // output "something matters"

A slight change to have the function receive the list of items and do the same operations:

fun <T: Any, R: Any> whenAllNotNull(vararg options: T?, block: (List<T>)->R) {
    if (options.all { it != null }) {
        block(options.filterNotNull()) // or do unsafe cast to non null collection
    }
}

fun <T: Any, R: Any> whenAnyNotNull(vararg options: T?, block: (List<T>)->R) {
    if (options.any { it != null }) {
        block(options.filterNotNull())
    }
}

Example usage:

whenAllNotNull("something", "else", "matters") {
    println(it.joinToString(" "))
} // output "something else matters"

These variations could be changed to have return values like let().

##Use the first non-null item (Coalesce)

Similar to a SQL Coalesce function, return the first non null item. Two flavours of the function:

fun <T: Any> coalesce(vararg options: T?): T? = options.firstOrNull { it != null }
fun <T: Any> Collection<T?>.coalesce(): T? = this.firstOrNull { it != null }

Example usage:

coalesce(null, "something", null, "matters")?.let {
    it.length
} // result is 9, length of "something"

listOf(null, "something", null, "matters").coalesce()?.let {
    it.length
}  // result is 9, length of "something"

##Other variations

...There are other variations, but with more of a specification this could be narrowed down.

Solution 2 - Kotlin

If interested here are two of my functions for solving this.

inline fun <T: Any> guardLet(vararg elements: T?, closure: () -> Nothing): List<T> {
    return if (elements.all { it != null }) {
        elements.filterNotNull()
    } else {
        closure()
    }
}

inline fun <T: Any> ifLet(vararg elements: T?, closure: (List<T>) -> Unit) {
    if (elements.all { it != null }) {
        closure(elements.filterNotNull())
    }
}

Usage:


// Will print
val (first, second, third) = guardLet("Hello", 3, Thing("Hello")) { return }
println(first)
println(second)
println(third)

// Will return
val (first, second, third) = guardLet("Hello", null, Thing("Hello")) { return }
println(first)
println(second)
println(third)

// Will print
ifLet("Hello", "A", 9) {
 (first, second, third) ->
 println(first)
 println(second)
 println(third)
}

// Won't print
ifLet("Hello", 9, null) {
 (first, second, third) ->
 println(first)
 println(second)
 println(third)
}

Solution 3 - Kotlin

You can write your own function for that:

 fun <T, U, R> Pair<T?, U?>.biLet(body: (T, U) -> R): R? {
     val first = first
     val second = second
     if (first != null && second != null) {
         return body(first, second)
     }
     return null
 }

 (first to second).biLet { first, second -> 
      // body
 }

Solution 4 - Kotlin

I like the idea of using a list filtering the null values, I usually do something similar when I'm working with the same type, but when there are multiple types, to avoid the values parsed as Any, I just do something like this

fun someFunction() {
    val value1: String = this.value1 ?: return
    val value2: Int = this.value2 ?: return
    ...
 }

It works and for me is important keep the type safety

Solution 5 - Kotlin

You can create an arrayIfNoNulls function:

fun <T : Any> arrayIfNoNulls(vararg elements: T?): Array<T>? {
    if (null in elements) {
        return null
    }
    @Suppress("UNCHECKED_CAST")
    return elements as Array<T>
}

You can then use it for a variable number of values with let:

fun example(first: String?, second: String?) {
    arrayIfNoNulls(first, second)?.let { (first, second) ->
        // Do something if each element is not null
    }
}

If you already have an array you can create a takeIfNoNulls function (inspired by takeIf and requireNoNulls):

fun <T : Any> Array<T?>.takeIfNoNulls(): Array<T>? {
    if (null in this) {
        return null
    }
    @Suppress("UNCHECKED_CAST")
    return this as Array<T>
}

Example:

array?.takeIfNoNulls()?.let { (first, second) ->
    // Do something if each element is not null
}

Solution 6 - Kotlin

Actually, you can simply do this, you know? ;)

if (first != null && second != null) {
    // your logic here...
}

There's nothing wrong in using a normal null-check in Kotlin.

And it's far more readable for everyone who will look into your code.

Solution 7 - Kotlin

For the case of just checking two values and also not having to work with lists:

fun <T1, T2> ifNotNull(value1: T1?, value2: T2?, bothNotNull: (T1, T2) -> (Unit)) {
    if (value1 != null && value2 != null) {
        bothNotNull(value1, value2)
    }
}

Usage example:

var firstString: String?
var secondString: String?
ifNotNull(firstString, secondString) { first, second -> Log.d(TAG, "$first, $second") }

Solution 8 - Kotlin

I actually prefer to solve it using the following helper functions:

fun <A, B> T(tuple: Pair<A?, B?>): Pair<A, B>? =
    if(tuple.first == null || tuple.second == null) null
    else Pair(tuple.first!!, tuple.second!!)

fun <A, B, C> T(tuple: Triple<A?, B?, C?>): Triple<A, B, C>? =
    if(tuple.first == null || tuple.second == null || tuple.third == null) null
    else Triple(tuple.first!!, tuple.second!!, tuple.third!!)


fun <A, B> T(first: A?, second: B?): Pair<A, B>? =
    if(first == null || second == null) null
    else Pair(first, second)

fun <A, B, C> T(first: A?, second: B?, third: C?): Triple<A, B, C>? =
        if(first == null || second == null || third == null) null
        else Triple(first, second, third)

And here's how you should use them:

val a: A? = someValue
val b: B? = someOtherValue
T(a, b)?.let { (a, b) ->
  // Shadowed a and b are of type a: A and b: B
  val c: C? = anotherValue
  T(a, b, c)
}?.let { (a, b, c) ->
  // Shadowed a, b and c are of type a: A, b: B and c: C
  .
  .
  .
}

Solution 9 - Kotlin

I have upgraded the expected answer a bit:

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

this makes this possible:

iflet("first", "sconed") {
    // do somehing
} ?: run {
    // do this if one of the params are null
}
    

Solution 10 - Kotlin

I solved this by creating some functions that more or less replicates the behavior of with, but takes multiple parameters and only invokes the function of all the parameters is non-null.

fun <R, A, B> withNoNulls(p1: A?, p2: B?, function: (p1: A, p2: B) -> R): R? = p1?.let { p2?.let { function.invoke(p1, p2) } }
fun <R, A, B, C> withNoNulls(p1: A?, p2: B?, p3: C?, function: (p1: A, p2: B, p3: C) -> R): R? = p1?.let { p2?.let { p3?.let { function.invoke(p1, p2, p3) } } }
fun <R, A, B, C, D> withNoNulls(p1: A?, p2: B?, p3: C?, p4: D?, function: (p1: A, p2: B, p3: C, p4: D) -> R): R? = p1?.let { p2?.let { p3?.let { p4?.let { function.invoke(p1, p2, p3, p4) } } } }
fun <R, A, B, C, D, E> withNoNulls(p1: A?, p2: B?, p3: C?, p4: D?, p5: E?, function: (p1: A, p2: B, p3: C, p4: D, p5: E) -> R): R? = p1?.let { p2?.let { p3?.let { p4?.let { p5?.let { function.invoke(p1, p2, p3, p4, p5) } } } } }

Then I use it like this:

withNoNulls("hello", "world", Throwable("error")) { p1, p2, p3 ->
    p3.printStackTrace()
    p1.plus(" ").plus(p2)
}?.let {
    Log.d("TAG", it)
} ?: throw Exception("One or more parameters was null")

The obvious issue with this is that I have to define a function for each case (number of variables) I need, but at least I think the code looks clean when using them.

Solution 11 - Kotlin

You could also do this

if (listOfNotNull(var1, var2, var3).size == 3) {
        // All variables are non-null
}

Solution 12 - Kotlin

one more idea based on the answer by @yole

fun <T, U, R> Pair<T?, U?>.toLet(body: (List<*>) -> R): R? {
    val one = first
    val two = second
    if (one == null || two == null)
        return null
    return if (one is Pair<*, *>) {
        one.toLet { a ->
            body(listOf(a, listOf(two)).flatten())
        }
    } else {
        body(listOf(one, two))
    }
}

so you can do the following

(1 to 6 to "a" to 4.5).toLet { (a, b, c, d) ->
    // Rest of code
}

Solution 13 - Kotlin

maybe it is a little late. But now it exist a library that addresses this specific need. It is Konad; have look at the maybe section

I will report here an example usage from the doc:

val foo: Int? = 1
val bar: String? = "2"
val baz: Float? = 3.0f

fun useThem(x: Int, y: String, z: Float): Int = x + y.toInt() + z.toInt()

val result: Int? = ::useThem.curry() 
   .on(foo.maybe) 
   .on(bar.maybe) 
   .on(baz.maybe)
   .nullable

// or even 

val result: Result<Int> = ::useThem.curry() 
   .on(foo.ifNull("Foo should not be null")) 
   .on(bar.ifNull("Bar should not be null")) 
   .on(baz.ifNull("Baz should not be null"))
   .result

Solution 14 - Kotlin

For any amount of values to be checked you can use this:

    fun checkNulls(vararg elements: Any?, block: (Array<*>) -> Unit) {
        elements.forEach { if (it == null) return }
        block(elements.requireNoNulls())
    }

And it will be used like this:

    val dada: String? = null
    val dede = "1"

    checkNulls(dada, dede) { strings ->

    }

the elements sent to the block are using the wildcard, you need to check the types if you want to access the values, if you need to use just one type you could mutate this to generics

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionDaniel Gomez RicoView Question on Stackoverflow
Solution 1 - KotlinJayson MinardView Answer on Stackoverflow
Solution 2 - KotlinDario PellegriniView Answer on Stackoverflow
Solution 3 - KotlinyoleView Answer on Stackoverflow
Solution 4 - KotlinMax CruzView Answer on Stackoverflow
Solution 5 - Kotlinmfulton26View Answer on Stackoverflow
Solution 6 - KotlinGrzegorz D.View Answer on Stackoverflow
Solution 7 - KotlinJonas HanssonView Answer on Stackoverflow
Solution 8 - KotlinMoshe BixenshpanerView Answer on Stackoverflow
Solution 9 - Kotlinyohai knaaniView Answer on Stackoverflow
Solution 10 - KotlinJonView Answer on Stackoverflow
Solution 11 - KotlinAmy EubanksView Answer on Stackoverflow
Solution 12 - KotlinblanNLView Answer on Stackoverflow
Solution 13 - KotlinLuca PiccinelliView Answer on Stackoverflow
Solution 14 - KotlinAlejandro MoyaView Answer on Stackoverflow