Suggestion: Better Nullability in functions


#1

EDIT: There were some issues with the original idea. So I have thought of a better idea.
The keyword ‘abort’ should abort the method, it is nested in. For example:

val v1 = 0 as Int?
val v2 = 1337 as Int?
val v3 = 7331 as Int?
    
println(maxOf(
    v1 ?: abort null,
    v2 ?: abort, // abort if value is null, if the funcion returns unit, then don't return anything, else return null
    v3 ?: abort
)) // prints null

This method is more verbose, and people wouldn’t overread it. If a programmer sees this kind of code, then it would be easy to understand, that it is a new feature, and could just google it. It would then also support labeled “breaks”, which would be very convenient, e. g.

val v1 = 0 as Int?
val v2 = 1337 as Int?
val v3 = 7331 as Int?
    
println(maxOf(
    v1 ?: abort@println null,
    v2 ?: abort@println,
    v3 ?: abort@println
))

Original idea starting here:

This is a small suggestion that would help me a lot. This Code doesn’t work because maxOf doesn’t accept nullable values.

val v1 = 5 as Int?
val v2 = 4 as Int?
val v3 = 6 as Int?
val v4 = 1 as Int?

println(maxOf(v1, v2))
println(minOf(v3, v4))

This would be fixable by using if statements like this, but this is ugly and not really modern (like Kotlin should be)

val v1 = 5 as Int?
val v2 = 4 as Int?
val v3 = 6 as Int?
val v4 = 1 as Int?
if(v1 != null && v2 != null)
    println(maxOf(v1, v2))
if(v3 != null && v4 != null)
    println(minOf(v3, v4))

If those were fields, then it would be even more complicated. But luckily there is the ?: return operator. So the following code works fine, BUT if i change any of those 4 values to null, main would return and not continue at all.

var v1 = 5 as Int? // Would not work as intended if v1 was null.
var v2 = 4 as Int?
var v3 = 6 as Int?
var v4 = 1 as Int?

fun main() {
    println(maxOf(v1 ?: return, v2 ?: return))
    println(minOf(v3 ?: return, v4 ?: return))
}

The following code works as expected, but is just ugly as hell.

var v1 = 5 as Int?
var v2 = 4 as Int?
var v3 = 6 as Int?
var v4 = 1 as Int?

fun main() {
    v1?.let { v1n -> v2?.let { v2n -> println(maxOf(v1n, v2n)) } }
    v3?.let { v3n -> v4?.let { v4n -> println(minOf(v3n, v4n)) } }
}

So this is my final suggestion. The using ? on a nullable object in a function should just don’t execute the function. Alternatively something like ?: exit would also work.

var v1 = 5 as Int?
var v2 = 4 as Int?
var v3 = 6 as Int?
var v4 = 1 as Int?

fun main() {
    println(maxOf(v1?, v2?)?)
    println(minOf(v3?, v4?)?)
}

#2

It’s unclear what should not be executed in this case, just maxOf() or the entire println(maxOf(...)) expression.
What would happen if the result of such call is assigned to some variable:

val max = maxOf(v1?, v2?)

Is the assignment not executed too and the variable max is left in uninitialized state?


#3

That’s an interesting idea, albeit not sufficiently specified.

How about this:

A function invocation will not be executed if any of its ? arguments are null at runtime. In this case, the function invocation expression will evaluate to null directly. Any such function invocation expression has automatically a nullable type, even if the function has a non-null return type.

Examples:

println(maxOf(null?, 5)) // prints null

println(maxOf(null?, 5)?) // does not print at all

val max: Int? = maxOf(null?, 5) // assigned to null

Above examples are simplified for clarity. The real benefit of this feature would show only when more then 1 argument is used with ?.


#4

I like the idea and I think it’s the most sensible way to handle this, but I’m afraid this would easily lead to unclear code. It’s hard to miss a single ? especially in longer expressions. This could lead to subtle bugs which are hard to find.

I guess I don’t like a single ? for the same reason kotlin has !! instead of !. Is this an argument for ??, maybe. I’m not sure.

Another problem to consider is how understandable this feature is to newcomers. My problem with this feature in general is not the feature itself, but the syntax and I’m not sure I like it.

Also I’m not sure how often this would really be useful. I’ve just done a short search through my current project: About 10k lines pure kotlin code and I’ve found maybe 2 places where this would be useful. That being said it’s hard to search for something like this as workarounds normally use multiple temporary variables, which is hard to query for.
That being said I try to structure my program in a way that I don’t use nullable types wherever possible, most nullable types deal with 3rd party libraries and are handled early.
Maybe others use this sort of feature more than I would.


#5

You’re right, I didn’t think about that. I was about to say the same thing as @fatjoe79 said (+1 for him). I’d like to add, that the operator should work even if the function accepts nullable values and that the operator should also work for properties. My example:
val vProp = 1234 as Int?

fun main() {
    val v = null
    example(v) // Should execute
    example(v?) // Should not execute even if example accepts nullable Ints
    vProp = v? // Should not execute because v is null
}

fun example(i: Int?) {
    println(i)
}

#6

Yes, I agree with you. ?? would properly be better than ?. I have thought that something similar to ?: return would also do this, like ?: exit or ?: abort. !! was also quite confusing for me, but a quick google search changed this. I know some nice cases, for example only executing a statement if a weakreference is set (it is an argument in the function). That’s quite useful for android. So it might be a bit confusing at the beginning, but completely worth it in my opinion.


#7

You could do:

var v1 = 5 as Int? // Would not work as intended if v1 was null.
var v2 = 4 as Int?
var v3 = 6 as Int?
var v4 = 1 as Int?

fun main() {
    println(maxOf(v1 ?: Int.MIN_VALUE, v2 ?: Int.MIN_VALUE))
    println(minOf(v3 ?: Int.MAX_VALUE, v4 ?: Int.MAX_VALUE))
}

Or, you could write your own functions that take nullables as arguments.


#8

That sucks.

  1. It’s ugly
  2. It’s hacky
  3. It doesn’t do what I want. I want it to either to print null or to not print at all if one value is null (depending on the example)
  4. It only works with this example and not with other cases.

#9

@H4xX0r1337, I would assume with nick like yours hucky solution shouldn’t bother you ;-D.
On the seriouse note:
I thought about solution with Int.MIN_VALUE and discurted it for the same reason. However wrap the function with your own to handle nullable type makes sense to me if you are using it in multiple places.

Also I think JetBrains guys will refuse this fueture because it is difficult for compiler to go back and suround the call with check while it already start parsing parameters. It is also makes code less readable if parameters are not simple token, but expression.


#10

I usually do not need this for the same function twice. If I would need it multiple times, then this would properly be the best workaround.

I have no idea how the kotlin compiler works, but I think that it already does go back in some cases (type inference, elvis operator, safe nullable call).

This might have been an argument for c / c++ or java, but this is a modern language. It already has null safety and other features for convenience, so why not implement this?

But this is basically the same for !!, and it is still there. Code is always getting less readable when it is getting more complex. This suggestion would make the code shorter and more concise.


#11

So, you want a new way of writing conditionals?

I don’t think that’s needed, it would be more confusing then useful.


#12

I can only quote myself. It would only be as confusing as !! or .let, .also, etc and would really make the language better.


#13

It seems like you are asking to zip nullables. You can do this yourself. There unfortunately isn’t a standard function to do this.

zipNullable(v1, v2) { a, b -> println(maxOf(a,b)) }

The implementation of zip can be ugly depending on how you want to support zipping two, three, four, etc arguments. You could implement this by doing something simple like

fun <T1, T2, R> zipNullable(o1: T1?, o2: T2?, zipFunction: (T1, T2) -> R?): R? {
    if (o1 != null && o2 != null) {
        return zipFunction(o1, o2)
    }
}

If you wanted to do this function twice, then you probably need to make the lambda a function so that you can just pass it in. I might have my syntax slightly wrong for this example. Sorry if that is the case.

val printMax = { a, b -> println(maxOf(a,b)) }
zip(v1, v2, printMax)
zip(v3, v4, printMax)

#14

This seems like a nice way to do this, but it would be bad to only support a finite range of arguments. This also doesn’t do “smart casting” and I need new variable names. I still think that my idea would be the best implementation.


#15

I still don’t know how I feel about this. My main concern is from a readability standpoint. The problem I have with it is that it changes when something could or couldn’t be called. So I have to read all the arguments to see if a function is called.

if (arg4 != null) {
   doWork(
       arg1,
       arg2,
       arg3,
       arg4
   )
}

Reads straight top to bottom.

doWork(
    arg1,
    arg2,
    arg3,
    arg4?
)

When reading, you only realize doWork won’t be called on some condition when you get to arg4. I think this would be worse with more arguments. If the specific problem you have is that you have to redefine variable names, this comes up a lot in functional programming languages. You can actually define functions without explicitly defining arguments, its called point-free or tacit programming. I don’t think Kotlin supports this very well, but I could be wrong. Using point free, you could write something more like

zip(v1, v2, compose(maxOf, println))

Given some compose operator/function. So the signature of the compose call would be
compose(f1: (A, B) -> C, f2: (C) -> D): (A, B) -> D. Compose basically take f1’s output and uses it as input to f2. Haskell, for example, has this so often that it is the ‘.’ operator. Haskell wouldn’t technically allow two arguments though.

Thinking about this new use of ‘?’ even more generally I think could make it even worse. What do I do if I don’t want to call a function if a number if negative or if it is the wrong type?

div(a, b?{ it < 0 })
div(a, b?{ it is Int })

I could revert back to filters/lets but that seems like what your trying to avoid.

div(a, b.takeIf { it < 0 }?)

vs

b.takeIf { it < 0 }
    ?.let { div(a, it) }
// or
if (b < 0) {
    div(a, b)
}

Which I will 100% give you is more verbose, but seems to be clearer to me at least.


#16

Yes, that’s right. It would be a little bit better, if I use ?? instead of ?.#

That’s not ugly and not hacky, so it’s a good idea, however it is even less readable for me (I never used Haskell before).

I am not trying to avoid lets, but I am trying to avoid creating a mess with lets, like this:

val v1 = 0 as Int?
val v2 = 1337 as Int?
val v3 = 7331 as Int?
    
v1?.let { v1n -> v2?.let { v2n -> v3?.let { v3n -> println(maxOf(v1n, v2n, v3n)) } } }

So I have thought of a better idea.
The keyword ‘abort’ should abort the method, it is nested in. For example:

val v1 = 0 as Int?
val v2 = 1337 as Int?
val v3 = 7331 as Int?
    
println(maxOf(
    v1 ?: abort null,
    v2 ?: abort, // abort without any value, if the funcion returns unit, then don't return anything, else return null
    v3 ?: abort
)) // prints null

This method is more verbose, and people wouldn’t overread it. If a programmer sees this kind of code, then it would be easy to understand, that it is a new feature, and could just google it. It would then also support labeled “breaks”, which would be very convenient, e. g.

val v1 = 0 as Int?
val v2 = 1337 as Int?
val v3 = 7331 as Int?
    
println(maxOf(
    v1 ?: abort@println null,
    v2 ?: abort@println,
    v3 ?: abort@println
))

#17

So I did some research and your not the only person to ever think of this. I found that reason has a syntax for this. https://reasonml.github.io/docs/en/function#explicitly-passed-optional which I am pretty sure might be a OCaml feature, but I am not sure. I still don’t know how I feel about this. :slight_smile:


#18

Interesting @dmstocking . Extending the syntax for named parameter invocation is better than what was proposed in this thread so far.