`foo?.bar.yolo.stop()` - Mixed Nullable & Non-Nullable Call Chain


#1

foo?.bar?.yolo?.stop()
Assuming bar and everything that follows is non-nullable, the remaining ?s are not required. Yet Kotlin enforces that…

So it could be like:
foo?.bar.yolo.stop()
which still returns a nullable value, same as current implementation but cleaner syntax.


Now, let’s assume yolo is nullable but not bar, so now the syntax becomes: foo?.bar.yolo?.stop().

Best part, this change is backward compatible.

Temporary workaround to avoid multiple null-checks:
val resultOfStop = foo?.run { bar.yolo.stop() }

If no result required, use apply. And IMHO the above solution doesn’t look that great. So please introduce Mixed Nullable & Non-Nullable Call Chain


Broken down example:

foo?.bar.yolo.stop() is equivalent to:

val yolo: Yolo? = foo?.bar.yolo
yolo?.stop()

#2

Not sure about that. How about constructs like a?.b?.c() ?: d. It works only because all links and the result are explicitly nullable. Also some links could be nullables themselves.


#3

What would the type of yolo be in val yolo = foo?.bar.yolo ? It cannot be Yolo because foo may be null, so the final value can be null too. That is why you have to repeat the question marks. The type of every step in the chain is nullable.


#4

Yes, you guys are right. foo?.bar.yolo.stop() will still return a nullable. But the syntax is cleaner.

so only val yolo: Yolo? = foo?.bar.yolo will compile. Not just Yolo. Internally it works the same as foo?.bar?.yolo.


#5

I am not an expert, but I think the compiler could do what you suggest. But it would change the semantics of the language, and in my opinion in a bad way, so I am very much against it.

. means that it is guaranteed that the property will always be accessed or function will always be invoked. Your suggestion makes this access or invocation optional.

I would really dislike having to go back to the Java days, where I constantly have to think about whether a property access or function invocation may or may not be on a null. Finding the first question mark in the call chain is easy, but it requires me to continuously scan call chains for question marks. Yes, the question marks are repetitive, and yes, the solution with run is a bit more verbose than a regular call chain. But working with nulls is supposed to hurt, so you improve your design to avoid them as much as possible.


#6

Let’s take a new example:
if (false) yolo.stop()
Now, stop() is never called. You have to follow the flow to realize it’s in a conditional branch. Now, I know this is a construed simplified example. Similarly whenever you use . (dot) operator, only whatever immediately precedes it is known to be non-nullable. But it doesn’t necessarily guarantee the execution of whatever follows.


#7

Sorry, I should have said “If the code reaches the point of the dot, then it is guaranteed …”.


#8

IMO the “safe navigation operator” is pretty dumb to begin with. The compiler easily has more than enough information to automatically determine when part of a call chain is nullable and then automatically insert safe-nav as needed.

Of course you will still need to declare the type as nullable in those cases, ie:

val yolo: Yolo? = foo.bar().yolo()

or else ensure that the value will always be assigned to something non-null, such as:

val yolo: Yolo = foo.bar().yolo() ?: new Yolo()

The only real downside is that you have to rely on the compiler/IDE to prevent you from screwing it up, but I don’t see that as being a particularly relevant downside for a language backed directly by IDEA.


#9

That’s pretty cool. In pure Kotlin, that would absolutely work.

But completely eliminating ? has serious Java interop implications.

Let’s say we get a value of type String! from some Java code. We don’t know if it’s null or not before a check. So now, if we don’t require ? for null check, there can be an unintentional NPE, when in fact we expected the runtime to catch null and prevent the NPE. It becomes ambiguous there.

So to workaround this interop issue, every single Kotlin code point that pierces into Java would have to be wrapped in a null-check, which I don’t think makes the best bytecode output because there can be too many unnecessary null-checks.

In which case, we could use @Nullable or @NotNull in the Java code appropriately. Which is again unfortunately not possible in libraries we don’t have control over.


#10

Hmm, that’s a good point. I guess in the worst case you’d end up with uncaught NPEs (ie no strict enforcement for platform types), but at the same time how is that any different from how platform types are already handled?

You could also make a kotlin-only wrapper for the jdk to get around that, but for other libraries you’d still be stuck with raw platform types.


#11

It is different in the sense that right now, if no ? is specified, it’s assumed to possibly NPE. But when ? is not present in the language ( or made optional ), it’s assumed to null-check everything and be NPE free by default which is tricky to handle and reason about.


#12

I partly agree, but what about the extension-property:

val Any?.bar get() = ""


#13

What about it?

val Any?.bar get() = "some string"
fun main(args: Array<String>) {
    println(null.bar)
}

Outputs:
some string


#14

This is really a problem with any language that tries to ride on java’s coattails while using stronger semantics than java does.

For pure kotlin (ie just as long as no platform types are involved) you can add ?s automatically with no penalty, and make them entirely optional.

For platform types there are two primary choices:

  1. Assume that all platform types are nullable, automatically inserting ?s.

For this case all platform types will result in a nullable output, and it may occur unnecessary overheads and null checks but otherwise guarantees correct results. It’s also not backwards-compatible

  1. Assume that all platform types are not nullable, unless the programmer explicitly adds ?s.

This case is backwards compatible and does not add any unnecessary overheads, but may throw NPEs if not correctly checked.

Either way is kind of sloppy since platform types aren’t explicit, but that basically only affects the programmer since the compiler does explicitly know the difference.

EDIT: and yes it is tricky to reason about, unfortunately, but still a lot less tedious.


#15

Kotlin is designed in a way that nullable variables are not very frequent. In those cases, variable is nullable it usually nullable by design and therefore additional ? is important hint that something could be null. I am not sure that removing is gives any advantage.
It is the same way about unsafe cast of nullable to non-nullable - !!. It is ugly, but idea is that you should never use it unless absolutely necessary.


#16
   class Hi(val bye : Bye = Bye("joe"))
   class Bye(val name: String)
   class Name(val value : String)
   fun main(vararg args: String){
       val hi : Hi? =  Hi()
       println(hi?.bye.name is String?)
   }

     val Bye?.name get()= Name("no name")

What does this return?
If the answer is true, then it breaks backwards-compatibility.
If the answer is false, does the code not become fragile (adding one method in your package changes all the implementations)


#17

Having a property nullable by design does not warrant having to use redundant null-checks (?). Kotlin is a pragmatic language that aims to remove redundancies.

I’m not sure even type-inference gives any advantage, other than syntactic sugar. So why not just go back to writing code in Java?

For the same reason, multiple ?s is a redundancy, the fat to be skimmed.

I’m not any expert and I’m writing everything with all the respect in the world.


#18

It should be Bye?. And hi.bye?.name. This example is not in accordance with the post. Please re-consider the example code.


#19

I agree that a lot of ?-s looks not so great. But I do not like the idea of delegating too much authority to the compiler. When you have a lot of language rules that work one way in one situation and the other way in another one, it will sooner or later byte you … somewhere. Kotlin team made a lot of effort to eliminate such context dependent situations compared to java (different rules for arrays and lists, if expressions etc).


#20

Whew. That example is problematic in terms of implementation, because even if you wanted to allow for run-time type checking you couldn’t really do it on a nullable type. ie the value is either null or a String, it can’t be a String? because if it’s null then there’s no allocated object to fetch type data from unless it’s resolved at compile time, which would cause that statement to become a hard-coded “true”.

Option types are, after all, technically a compile-time feature with no semantic value at run time. You could technically evaluate it to a constant but that’d also be fairly pointless except for consistency (which I guess is worthwhile). If that breaks backwards compatibility then perhaps it wasn’t well thought-out in the first place though. :frowning:

Personally I’m not a fan of type inference, but NPEs are easily the #1 biggest PITA in java so strong null semantics are heartily welcomed.

idk about all that (guess I’ll have to do more research). I do know that null and false are considered to be equal in kotlin when using ?: though, which is kind of iffy.