class Person {
String name
int age
}
Person getPerson() { null }
Person p = getPerson()
def result = p?.age
17 April 2013
Tags: groovy null programming
Yesterday, I worked on a bug reported on the static compiler of Groovy. It appeared that the underlying problem was related to how null-safe invocations are handled when the expected return type is a primitive. Let’s take an example:
class Person {
String name
int age
}
Person getPerson() { null }
Person p = getPerson()
def result = p?.age
Now the question is, what is the value of result? If you run this in the Groovy console, the result would be null. This is compatible with the assumption that the null-safe invocation operator (?.) always return null if the receiver of the message is null (here, p is null, so result is null). The definition is pretty easy, but it gets more complicated if you slightly modify the code:
int result = p?.age
By explicitely setting the type to int, executing this would throw an error, stating that you cannot convert null to int. It makes sense knowing that the null-safe invoker is supposed to return null if the receiver is null, but it starts getting strange as if p is not null, the assignment is perfectly valid since getAge() is expected to return a primitive type… This means that unlike the ``normal'' invoker which is guaranteed to keep the method return type untouched, the null-safe invoker does not honor the method signature.
What if the null-safe invoker was aware of the return type? In null-safe'', you must think about what the
safe'' part stands for. It’s definitely the receiver, because you want the invocation to be safe (not failing) if the receiver is null. If the receiver is null, then return null. This means that because p is null, it chooses to return null, independently of the method that was supposed to be called. Here, the method was getAge(), which is supposed to return a primitive type. As null is not a primitive, I would expect the null-safe invoker to return a default value compatible with the primitive type. This means that here, I would expect the null-safe invoker to return 0. Now, what is the problem with returning a default value? First of all, we’re in a dynamic world. This means that when p?.age is executed, the target method hasn’t been chosen, because you need to know the runtime type of p to determine what method will eventually be called. As p is null, the dynamic runtime doesn’t know the type of p, so has no idea that calling age would return a primitive type. Conclusion, in a dynamic world, the null-safe invoker must always return null, even if the expected method would return a primitive…
Now what if we’re in a pure static world? In that case, the method to be called is chosen at compile time, given the inferred type of p. It means that unlike the dynamic runtime, the static compiler, at this point, knows that p.age is p.getAge() which returns a primitive type. So it is capable of handling the null-safe invocation with what I think is better, semantically speaking: null-safe invocation only checks the receiver, and returns a value which depends on the return type of the method being invoked. So a static compiler is able to return 0 instead of the non-pritive null. What is funny is that I asked, on Twitter, what people expected from the result of p?.getAge() if p is null. Everybody answered null. So it’s clear that my way of thinking is not mainstream, but I’m ok with that. I just find it awkward that an operator which is supposed to act on invocation is also capable of altering the return type…
Anyway, even if it’s possible for the static compiler to return a default value, the fix I pushed doesn’t do that. It will always return null. The main reason for doing that is not that it was easier to fix (it’s quite the opposite), but that it keeps the semantics of statically compiled Groovy equal to those of dynamic Groovy. As it’s not possible for the runtime to know what the method would return, always returning null is fine, even if some situations (static compilation), you know a bit more what would happen :)