Feature Request: Null Check for Arguments in Function Invoke

Was thinking about using the null check operator to conditionally invoke functions. For example:

val result = someFunction(a?, b, c?)

which would be equivalent to:

val result = a?.let { aNotNull ->
  c?.let { cNotNull ->
    someFunction(aNotNull, b, cNotNull);
  }
}

Except a lot cleaner. This syntax would also work fine:

val result = someFunction(?a, b, ?c)

I feel this would scale much more nicely for functions with multiple nullable arguments, rather than chaining let statements together

3 Likes

Extension on function reference:

inline fun <T1, T2, T3, R> KFunction3<T1, T2, T3, R>.callIfNotNull(a: T1?, b: T2?, c: T3?): R? {
    a ?: return null
    b ?: return null
    c ?: return null

    return this(a, b, c)
}

::someFunction.callIfNotNull(a, b, c)

(however, not sure whether the call will be optimized or not).

multi-let - more robust, less consise:

inline fun <T1, T2, T3, R> let(a: T1?, b: T2?, c: T3?, block: (T1, T2, T3) -> R): R? {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }

    a ?: return null
    b ?: return null
    c ?: return null
    
    return block(a, b, c)
}

let(a, b, c) { a, b, c -> someFunction(a, b, c) }
// or
let(a, b, c, ::someFunction)
1 Like

Personally, I rarely come across such situations. I prefer just null check and return from the function as early as possible, therefore, large numbers of nullable arguments do not accumulate in the deep of the data flow.

3 Likes

Both of these solutions are fine. My only issues with it is you need to define a separate function for every variable count, (also it’s slightly less concise).

I think my feature would add convenience. When you think about it, the kotlin devs didn’t need to include the Elvis operator either, they could have let users implement it themselves with extension functions. It’s all about convenience. I don’t see any downsides to having this extra operator usage.

1 Like

I think this exact request has been made else where before–possibly within a thread about capturing multiple variables.

The core of the issue is capturing multiple variables. Personally I think there are better options than null-calls to methods suggested here but I’ll have to dig them up later and link them.

1 Like

If I remember correctly, the main issue was that it looks too much like existing constructs and it behaves too differently from them (nothing in the language bypasses function calls from the arguments, except exceptions, but those are easy to debug since they crash the whole thing).

I think the solution above with utility functions is short enough that it’s easy to read, and it’s fairly clear that it’s something specific (maybe we could petition to add one of those functions to the standard library?)

1 Like

I totally agree with this.

I have a lot of concerns with the proposed syntax, as it adds the possibility that code starting with val x = someCall(.... will not actually call the function. This doesn’t happen with anything else in the syntax, and I believe it would be too unexpected if it did.

I also don’t recall needing this ever. It’s usually nice to keep nulls out, and when they are present they should be handled explicitly. What’s so bad about a plain if expression here?

val expression = if (a == null || b == null) null else someFunction(a, b, c)

Nothing wrong with it, but as I said above, one of the great things about Kotlin is how convenient it is, and the quality of life functions they have. They didn’t need to add the Elvis operator either since you can accomplish that with an IF statement, but if your goal is to scale down the language to the minimum possible functionality, then I can name quite a few other Kotlin features that we can start removing.

I do agree with this though

I think my function call issue is a subset of being able to capture multiple variables. Maybe a multi-let function would be more useful?

let(a, c){ a, c -> someFunction(a, b, c) }

IMO the elvis operator adds “convenience” both on the writing side AND the reading side. The expressions with elvis look specific and have their specific behaviour, so I (personally) never had any occurrence where I found it misleading or was surprised in any way.

I believe the feature suggested here is different in this respect. I find this tiny extra ? character easy to miss, partly because it looks like other things in the syntax. Also, it behaves differently, especially in the sense that ?-based expressions usually don’t impact a wider scope than the expression itself. For instance I find the following code pretty misleading, especially if variable names are real-life multi-letter names:

val r1 = someFunction(a, b, c)
val r2 = someFunction(a, b?, c)
val r3 = someFunction(a, b?.prop, c)
val r4 = someFunction(a, b?.prop?, c)

Kotlin aims at being concise, but strives for explicitness, and even though there is a difference between b, b?, b?.prop, and b?.prop?, I don’t think this syntax makes it obvious to the reader in one glance which of these calls could actually be skipped.

We could make the same case against using the elvis + return in a function call, by the way:

val result = someFunction(a, b ?: return 42, c)

I would much rather have an intermediate variable here. But at least, the return keyword stands out. I find the use of elvis + return convenient in other places but a bit dangerous here. The proposed syntax however only concerns function parameters, and this place IMO is dangerous for readability. Maybe it’s just a matter of habits.

4 Likes