is there a reason behind the ‘eager’ evaluation of...
# announcements
m
is there a reason behind the ‘eager’ evaluation of safecalls in kotlin?
Copy code
data class B(val i: Int)
data class A(val b: B)

class Test {
    var a: A? = A(B(1))
    fun get(): Int {
        return a?.b.i ?: -1
    }
}
the expression
a?.b.i
must in kotlin be written as
a?.b?.i
but in reality, if
a
isnt null, we know that
b
must contain
i
(in Swift i think the former expression is valid)
r
I find the easiest way to think about it is this: the expression is evaluated from left to right, with
.
denoting normal access and
?.
denoting a safe call (i.e. give me the right hand side if the left hand side is non-null, and null otherwise). If
a
was null then
a?.b.i
would be equivalent to
null.i
which will always fail, whereas
a?.b?.i
would be equivalent to
null?.i
which then successfully collapses down to
null
. Swift is very similar, with https://docs.swift.org/swift-book/LanguageGuide/OptionalChaining.html giving an example of
john.residence?.address?.street
. There is a nice list of examples for various languages on wiki https://en.wikipedia.org/wiki/Safe_navigation_operator
Perhaps the missing piece in the jigsaw is that
?.
is left-associative. So
a?.b?.c
is
(a?.b)?.c
m
looking at the bytecode it actually checks if the result of A.getB() is null or not, something that is clearly declared as non null in class A
in swift, it works as expected
Copy code
struct B {var i: Int}
struct A {var b: B}

class Test {
    var a: A? = A(b:B(i:1))
    func get() -> Int {
        return a?.b.i ?? -1
    }
}
notice that I don’t need a safe call between
b
and
i
r
That is interesting, so Swift can shortcut the rest of the chain if they aren't declared as optional types. That is certainly a choice but not one ive seen in other languages. I see your point regarding the bytecode. Hopefully someone else has some insight? May be a compiler optimization waiting to happen
h
If a was null then a?.b.i would be equivalent to null.i
Well, no, according to the docs, if a is
null
, then the statement should return already at that point, so there should be no
null.B.i
. I must agree with OP here, that demanding a safe call on B in this case seems odd.
m
but the compiler can optimize it to
if a==null return -1 else return a.b.i
since if a isnt null, it will always contain
b
and b will always contain
c
(sorry, missed your last post)
Looking at the bytecode, there could be some optimizations there. Right now it inserts an unnecessary null check there as well.
r
@hallvard can you point out where in the documentation it suggests it should shortcut, I can't find any statement in https://kotlinlang.org/docs/reference/null-safety.html which explicitly says this The statement "Such a chain returns null if any of the properties in it is null" for example immediately follows a chain of the form
a?.b?.i
, so doesn't suggest a shortcut. See also https://kotlin.github.io/kotlin-spec/#navigation-operators which suggests it is left-associative and does not suggest a shortcut.
h
I'm sorry to say that web page was the documentation I had in mind, and my reference was purely by memory.
In a chain like
a?.b?.i
, where does the null come from if
a
is null? It's not very logical to imagine that it comes from the
b?.
part, as already
a?.
is null. So that's the firs and most obvious reason for my believing so. And also the reasoning behind the original quastion, if I am not mistaken.
m
The created bytecode does shortcut, so if
a
is null it will jump to
return -1
after that it could just get
b
and
i
but it gets
b
and performs another nullcheck.
Copy code
public final get()I
  L0
   LINENUMBER 22 L0
   ALOAD 0
   GETFIELD tst/Test.a : Ltst/A;
   DUP
   IFNULL L1
   INVOKEVIRTUAL tst/A.getB ()Ltst/B; <-- getB is marked as non null so no need to test for null here
   DUP
   IFNULL L1
   INVOKEVIRTUAL tst/B.getI ()I
   GOTO L2
  L1
   LINENUMBER 22 L1
   POP
   ICONST_M1
  L2
   IRETURN
  L3
   LOCALVARIABLE this Ltst/Test; L0 L3 0
   MAXSTACK = 2
   MAXLOCALS = 1
h
Yes, it seems the compiler doesn't observe the «nonnullity» of
val B
inside
A
. Interresting, but too time-consuming for me now. I think oyu should raise an issue for this on the JB issue tracker, maybe we'll get a nice explanation. Thanks for the chat!