Null and Java interop
The way Ceylon handles null
is one of the big attractions of
the language. Ceylon features a typed null value. The object
null
is just an instance of the perfectly ordinary class
Null
from the point of view of the type system. But when the
compiler backend transforms Ceylon code to Java bytecode or
JavaScript, the class Null
is eliminated and the value null
is represented as a native Java or JavaScript null
at the
virtual machine level.
This is great for interoperation with native Java or JavaScript.
However, when designing our Java interop, we had to take into
account that Java's null
isn't typed, and that the
information about whether a Java method might return null
,
or whether a parameter of a Java method accepts null
is
"missing" and not available to the Ceylon typechecker. I think
we've done the right thing here, but the approach we took has
surprised a couple of people, so I guess it's well worth
calling explicit attention to what we've done and why.
Consider the following useful method of java.lang.String
:
public String toUpperCase(Locale locale) { ... }
By checking the implementation of this method, I determined that:
-
toUpperCase()
never returnsnull
, and that - the argument to
locale
must not benull
.
Unfortunately, this information isn't available to the Ceylon
typechecker. So perhaps you might expect that Ceylon would take
a very heavyhanded route, treating the return type of
toUpperCase()
as String?
, and forcing me to explicitly
narrow to String
using an assertion:
assert (exists uppercased = javaString.toUpperCase(locale));
In fact, this is not what Ceylon does. This approach would make interoperation with Java a nightmare, resulting in code full of hundreds of useless assertions. You would need an assertion essentially every time you call a native method.
Worse, the "heavyhanded" approach would prevent me from passing
the value null
as an argument to a parameter of a Java method.
What would it mean to "assert" that a method parameter accepts
null
? That's not an assertion that can be tested at runtime!
Nor would the "heavyhanded" approach in practice even protect
me from NullPointerException
s. As soon as I call native Java
code, a NullPointerException
can occur inside the Java code I
call, and there's nothing Ceylon can do to protect me from that.
So instead, Ceylon takes a "lighthanded" approach at the boundary between Java and Ceylon, letting you write this:
String uppercased = javaString.toUpperCase(locale);
The typechecker knows this is a Java method, and that the
information about null
is missing, so it assumes that you know
what you're doing and that toUpperCase()
never returns null
.
But what if you've got it wrong, and toUpperCase()
does
return null
at runtime? We would not want that null
value to
propagate into Ceylon values declared to contain a not-null
type!
What the Ceylon compiler does is insert a null
check on the
return value of toUpperCase()
, and throws a NullPointerException
to indicate the problem. If you ever see that NullPointerException
,
at runtime, you're supposed to change your code to this:
String? uppercased = javaString.toUpperCase(locale);
This is essentially the best Ceylon can do given the information available to it. The NPE is not a bug! It's not even really a "gotcha".
Still, the NullPointerException
might look like a bug to novice
user. (Indeed, Stef recently noticed someone describe it as a bug
in a conference presentation.) So I'm asking myself how we can
make this less surprising. There's a range of options, including:
- add a better error message to the
NullPointerException
, - throw an
AssertionException
instead of aNullPointerException
, or even, perhaps, - following the approach we use for JavaScript interop, require
you to surround "lighthanded" code in a special construct that
suppresses the "heavyhanded" typechecks,
dynamic Null { ... }
or something.
I lean towards the first of these options, I suppose.
Whatever we do, we need to document this better. Right now, the documentation is kinda hidden.