Kotlin null check for multiple nullable var's


#21

Hey there,

i’ve run in this problem as well.
Before I wrote Stuff in Kotlin I used Scala and Clojure to get things done, both of them have a more elegant way to deal with multiple NonNull Values for Collection Operations and Lets, then Kotlin has out of the box. So I decided to use the power of ExtensionFunctions and wrote my own Library to make it more elegant and readable.

You can find the library on Github: https://github.com/stupacki/MultiFunctions

The project is still in progress and I need to add a discription to use it but here a litte example to fix your problem.

import io.multifunctions.letNotNull

Quad(userApi.get(userId),
     ordersApi.get(userId),
     favoritesApi.get(userId),
     notesApi.get(userId))
    .letNotNull { user, orders, favorites, notes ->
    
         HttpResult.renderPage(user = user,
                               order = orders,
                               favorites = favorites,
                               notes = notes)

    }

In this example the lambda returns null in case one of the accessed apis return a null object, the let will return a null as well.

Currently the lib is able to handle 6 parallel objects/collections with the following functions:

  • let
  • letNotNull
  • map
  • mapNotNull
  • forEach
  • forEachNotNull
  • mapIndexed
  • mapIndexedNotNull
  • withIndex

I hope this will help you with this problem


#22

Despite possible performance impacts, local vals is the best solution. If the variable is mutable and accessible to outer scopes then the only way to guarantee that it hasn’t changed is making a local copy. Even null checking using ?.let {} for single variables does this:

fun test() {
    name?.let { doSomething(it) }
}

compiles to the equivalent java code:

public final void test() {
    String var10000 = this.name;
    if(this.name != null) {
        String var1 = var10000;
        this.doSomething(var1);
    }
}

(Note: the kotlin compiler does optimise this when it can be sure that the variable won’t change. If name was a local var instead of a property, the compiler wouldn’t make an additional local variable

Swift has a similar problem with null checking multiple vars and solves this by making it easy to declare local values within if statements:

// Swift code 
if let name = name, let age = age {
    doSth(name, age)
}

A similar syntax in Kotlin could be the following:

if (val name = name, val age = age) {
    doSth(name, age)
}

Which would compile to the equivalent java code:

final String name = this.name;
if (name != null) {
    final Integer age = this.age;
    if (age != null) {
        doSomething(name, age);
    }
}

The usage of the comma instead of && is to differentiate it from normal if statements in that everything should evaluate to non-null, the || operator is not allowed and do not make sense:

if (val name = name || val age == age) { /* Defeats the point of null checking */ }

Like with the ?.let{} case, it would be possible for the compiler to optimise the creation of the local variables, only creating them if required. Also like the it in the ?.let{} block, any values declared in the scope of the of the if statement and it’s following block would be immutable.

The syntax could be generalised as follows:

if (val var1 = <expression1> ,val var2 = <expression2>, val varN = <expressionN>) {
    // Do something with var1, var2, varN
}

Where <expressionX> is any expression that returns a nullable type

Which compiles to the equivalent java code:

final Type1 var1 = <expression1>;
if (var1 != null) {
    final Type2 var2 = <expression2>;
    if (var2 != null) {
        final TypeN varN = <expressionN>;
        if (varN != null) {
            // Do something with var1, var2, varN
        }
    }
}

It may seem a bit much, but it is almost identical to how multiple consecutive safe null calls are compiled. e.g: foo?.bar?.baz

The benefit however is that is that you can use earlier values in later expressions. In the example below p1 and p2 have already been null checked so we can access their age property directly.

fun whoIsOldest(person1: Person?, person2: Person?) {
    if (val p1 = person1, val p2 = person2, val age1 = p1.age, age2 = p2.age) {
        when {
            age1 > age2 -> print("$p1 is oldest")
            age1 < age2 -> print("$p2 is oldest")
            else -> print("$p1 and $p1 are the same age")
        }
    } else {
        print("Your need two people to compare ages")
    }
}

And because the expressions can be anything that returns a nullable type it can also be used with safe casts:

if (val child = person as? Child, val car = child.favouriteToy as? Car) {
    car.race()
    print("$child is racing their toy $car")
}

A additional improvement that could be made which is also available in swift is allowing boolean expressions alongside the nullable expressions. It’s a fairly common use case to check if something is non-null and then perform an additional check to see if it is appropriate to use.

if (val childAge = person.child?.age, childAge >= 6 && childAge < 18) {
    print("$person has to drop their child at school each weekday")
}

Here the use of the comma instead of && reinforces the requirement that every expression needs to evaluate to non-null and true, no || operators are allowed.


#23

Minor performance issues aside, this seems like an ideal solution.

Especially the extra step of being able to have boolean expressions as this covers a huge number of use cases I’ve seen.

Yes you COULD declare these vars your self, but then they (probably?) couldn’t be optimized away. And it feels pretty boilerplatey, which is antithetical to Kotlin it seems.

So tl;dr +1 from me. would LOVE to see this implemented in the language :slight_smile:


#24

Which means that the semantics of Kotlin change in these ways:

  • val childAge = person?.child?.age results in type Int? in normal declarations, but in type Int in if statements.
  • Not null is equal to false, which introduces C syntax that allows multiple types in conditionals.

Not that I think these are blocking issues, but the inconsistencies are something to consider.


#25

To clarify I’m not advocating that C style implicit boolean conversions should be added. null should not be treated as false in other circumstances: if (person?.name) {...} shouldn’t be possible.
Rather I’d think of it as not-being-able-to-assign-nullable-to-non-null equals false


#26

This again makes the language more inconsistent: Some constructs only work in a limited number of situations, and not in other similar situations. I can, for example, imagine people trying to use this in a while-statement. The (justified) expectation is that if you can use something in situation A, you can also use it in similar situation B. So if people see the expression in the if statement above, it is reasonable to expect them to be able to do this:

 val hasToBeDroppedOffAtSchoolOnWeekdays = val childAge = person?.child?.age, childAge >= 6 && childAge < 18

#27

We could have a new control structure instead of if().

guard() or something, so it’s clear this is a special case. Much like the for loop’s syntax is somewhat strange and wouldn’t work inside an if()


#28

I don’t see any problem with also allowing while statements to use this syntax, as it’s just another type of conditional flow control. (swift also allows it’s version of this syntax in this situation)

val ride = RollerCoaster()
while (val person = personQueue.getNextPerson(), person.height > MIN_HEIGHT) {
	print("$person is riding the roller coaster")
	ride.add(person)
}
ride.start()

The equivalent java code would be a bit more complicated though compared to the if version.


There are already Kotlin syntax rules which would indicate that statement wouldn’t be possible. The statement above is pretty similar to following, which won’t compile:

val bar = val foo = "baz" // compiler error

Also assigning T? to a T always requires some kind of extra context for it to work, either by explicitly null checking and relying on the smart cast or using an elvis operator to provide a fallback value. The proposed syntax is just another way to add context to let the assignment work


I don’t see it as inconsistent, it’s just the type inference working as it should. Without it, the if statements would be written as follows look like follows:

if (val age: Int = person.age) { ... }

Where it works just like smart casting or the elvis operator, extra context is provided to ensure that age will not be null and type inference can eleminate the need to explicitly specify the non null type.

I’d even argue that this syntax is even easier to understand and more consistent than smart casts.

  • Its easier to understand because it forces the assignment to be next to the control structure making it easy to see why the assignment is possible. With smart casts there could be 100’s if lines of code between the null check that makes it possible and the actual cast. There’s also two cases where smart casts can occur:

    if (foo != null) { foo.bar() }
    if (foo == null) return; foo.bar()
    

    At least with the first kind the curly brackets can sometimes provide a scope around where the check might have been performed, but with second kind it could be anywhere between the smart cast and the var’s declaration.

  • It’s more consistent because to works regardless of where it is. One of the problems showed in the example by the OP was a smart cast not working despite performing a null check. Take for example:

    if (foo?.bar != null) { foo.bar.baz() }
    

    The smart cast works inconsistently depending on whether foo is local, a parameter, a var property, a val property, a val property with an custom getter, a val property with an open getter, a package level var, or a package level val. Then multiple this by all the different cases that apply to bar as well.

    On the other hand the following always works, regardless of where it is placed:

    if (val bar = foo?.bar) { bar.baz() }

#29

Maybe the inconsistency could cause a problem for a small fraction of people for a moment before they learn why.

But the multiple null check issue will effect every single Kotlin user. So even if the inconsistency is an issue (which I don’t think it is, especially if you create a new keyword that it is only valid inside of) this is still a much greater good,


#30

What about:

    fun notNull(vararg args: Any?, action: () -> Unit) {
        when {
            args.filterNotNull().size == args.size -> action()
        }
    }

    val a = 1
    val b = "something"
    var c: Float? = 2f

    notNull(a, b, c) {
        println("Almost safe!")
    }

    c = null

    notNull(a, b, c) {
        println("Doesn't print ...")
    }

#31

Shameless plug:
https://youtrack.jetbrains.com/issue/KT-20294

TL;DR: the error is not actually preventing any bugs, yet is a major nuisance; it should just become a (suppressable) warning instead. Please voice your support for this change!


#32

Can’t we have an extended elvis operator like below? :

val result = operationX(a?, b?, c?) ?: getDefaultValue()

Which would be the short syntax for:

val result = if (a != null && b != null && c !=null && operationX(a, b, c) != null) 
                operationX(a, b, c) 
             else 
                getDefaultValue()

This is the syntax which Advanced Java Folding plugin (from Jetbrains itself) is using to fold ternary operators in Java codes. This plugin will fold this:

return fixedPayment!=null? service.trs(p -> p.saveOrUpdate(fixedPayment)).map(Res::result) : null;

to this:

return service.trs(p -> p.saveOrUpdate(fixedPayment?)).map(Res::result) ?: null;

#33

There’s some heated discussion there, I don’t want to bloat bugtracker with flames, but I’m very against this kind of change. Null-safety and smart casts are awesome in Kotlin and it makes me much more productive. I understand where this problem comes from, I even wrote something about it myself, but I don’t think that there’s any sane way to make it much better than now.

  1. Sacrificing null-safety or type-safety is just not an option at all. I love that Kotlin’s guarantees are hard to break and almost impossible with normal code.
  2. Changing, whether one function compiles, analyzing another function bodies is just bad. I’m sure that it’s bad from compiler performance perspective, but first of all it’s bad from cognitive load. I’m changing implementation of one function and suddenly 10 other functions do not compile. That’s very fragile construction.

With that said, I think that there still could be a way to improve this situation.

  1. Explicit construction to use temporary local variable as a property alias. Now one could modify this variable and use it as a local variable, but changes will propagate to property (but not other way), so external modifications won’t be visible.

  2. If property is not open and it’s trivial (without explicit getter/setter), compiler could treat this property as a local variable until some function call occurs. Of course there’s still danger of multithreading modifications, but I think that if access to a variable is not guarded by proper synchronizations, it’s a bug anyway. And any synchronization results to some function call, which will disable smart casting.


#34

I’ve just discovered the cross-module smart cast limitation.

It’s very common to have your data contracts in another module, which means this issue is an even bigger problem.

Currently the only solution I see is creating tons of temporaries, which seems so counter to Kotlin’s philosophy of no boilerplate.


#36

Call me crazy, but I don’t find a dumb cast especially onerous here:

class Person(var name:String? = null, var age:Int? = null){
    fun test(){
        if (listOf(name, age).all { it != null }) {
            doSth(name!!, age!!)
        }
    }

    fun doSth (someValue:String, someValue2:Int){

    }
}

#37

What about this?

class A(var a: A? = null, var b: A? = null) {
  fun test() {
    val (a, b) = a to b
    if (a != null && b != null) { ~ }
  }
}

#38

the more I use kotlin, the more I miss groovy truth.

what about this:

class Person(var name:String? = null, var age:Int? = null){
    fun test(){
        if(name != null && age != null)
            doSth?(name, age) //no need to cast
    }

    fun doSth (someValue:String, someValue2:Int){

    }
}

doSth?(…) is consistent and concise.