Ceylon Might Just be the Only (JVM) Language that Got Nulls Right
Here we go again. THAT TOPIC.
But hang on. The approach discussed here (and in the Ceylon language) is not something you see every day. At the same time, it is very cunning.
Nulls are baked into the language
… or so it may seem. Indeed, in Ceylon, like in Kotlin (and possibly many other languages), there is a special type “annotation” that you can postfix to any reference type in order to make it nullable. For instance:
String firstName = "Homer"; String? middleName = "J"; String lastName = "Simpson";
In the above example, both firstName
and lastName
are mandatory values that can never be null
, whereas middleName
is optional. Most languages that support the above then ship with special operators to access the optional value, e.g. .?
in Ceylon and also in Kotlin.
// Another optional value: Integer? length = middleName.?length; // A non-optional value: Integer length = middleName.?length else 0;
So, what is it about Ceylon that works so smoothly?
The thing that Ceylon got very right is the fact that all of the above is just syntactic sugar that is:
- Easy to use
- Maps well to our mindset, where
null
still is a thing - Can interoperate with Java
- Doesn’t introduce cognitive friction
For us Java folks, we can still pretend that null
is an OK-ish, hard to avoid thing (as we’ve claimed before on this blog). But what is null
really? Is it the absent value? The unknown value? The uninitialised value?
Java only has one null
thingy, and it is (ab-)used for all of the previous things, and more, when in theory, it is only really the uninitialised value, nothing more. On the other hand, when working with JDBC (and thus, SQL), it implicitly means the unknown value (with all the related caveats).
In Ceylon, however, Null
is a special type, similar to Void
in Java. The only value that can be assigned to the Null
type is null
:
// Ceylon Null x = null; // Java Void x = null;
But the big difference is, null
cannot be assigned to any other type! Wait. Couldn’t we assign null
to String?
… ? Of course, the following is possible in Ceylon:
String? x = null;
But why is this possible? Because String?
is just syntax sugar for String|Null
, a union type, i.e. a type that is either the String
type or the Null
type.
Huh, what are union types?
Let’s look at this more closely. When in the jOOQ API you want to work with SQL functions and expressions, there is always a great set of overloads that provide you with a standard version, and a convenience version where you can pass a bind variable. Take the equals operator, for instance:
interface Field<T> { Condition eq(Field<T> field); Condition eq(T value); }
The above overloads allow you for writing things like the following, without needing to think about the distinction between a SQL expression and a Java bind variable (which is ultimately also a SQL expression):
// Comparing a column with bind variable .where(BOOK.ID.eq(1)) // Comparing a column with another column expression .and(BOOK.AUTHOR_ID.eq(AUTHOR.ID))
In fact, there are even more overloads, because the right hand side of a comparison operation can have other expressions as well, for instance:
interface Field<T> { Condition eq(Field<T> field); Condition eq(T value); Condition eq(Select<? extends Record1<T>> query); Condition eq(QuantifiedSelect<? extends Record1<T>> query); }
Now, the same set of overloads needs to be repeated for not equals, greater than, greater or equal, etc. Wouldn’t it be nice to be able to express this “right-hand-side” thingy as a single, reusable type? I.e. a union type of all of the above types?
interface Field<T> { Condition eq( Field<T> | T | Select<? extends Record1<T>> | QuantifiedSelect<? extends Record1<T>> thingy ); }
Or even
// This is called a type alias. Another awesome // Ceylon language feature (pseudo syntax) alias Thingy => Field<T> | T | Select<? extends Record1<T>> | QuantifiedSelect<? extends Record1<T>>; interface Field<T> { Condition eq(Thingy thingy); }
After all, that’s also how the SQL language is defined. Heck, that’s how any BNF notation defines syntactic elements. For instance:
<predicate> ::= <comparison predicate> | <between predicate> | <in predicate> | <like predicate> | <null predicate> | <quantified comparison predicate> | <exists predicate> | <unique predicate> | <match predicate> | <overlaps predicate>
OK, granted, a syntactic element is not strictly the same thing as a type, but the intuitive perception is the same.
Oh, and Java has union types, too!
In a brief flash of revelation, the Java 7 expert groups added support for union types in exception handling. You can write things like:
try { ... } catch (IOException | SQLException e) { // e can be any of the above! }
Back to Ceylon and NULL
Ceylon has gotten Null
right. Because, historically, a nullable type is a type that can be the “real” type or the “null” value. We want that. We Java developers crave that. We cannot live without the soothing option of this kind of optional.
But the excellent thing about this approach is that it is extendable. What if I really need to distinguish between “unknown”, “uninitialised”, “undefined”, “42”? I can. Using types. Here’s a String that can model all of the aforementioned “special values”:
String|Unknown|Uninitialised|Undefined|FortyTwo
And if that’s too verbose, I just assign a name to it
interface TheStringToRuleThemAll => String|Unknown|Uninitialised|Undefined|FortyTwo;
But it cannot be Null
. Because I don’t want it to be that value, that is everything and nothing. Are you convinced? I bet you are. From now on:
Don’t trust any language that pretends that the Option(al) monad is a decent approach at modelling null. It isn’t.
― me. Just now
Why? Let me illustrate. Kotlin/Ceylon/Groovy style syntax sugar using the elvis operator (regardless of the backing null semantics):
String name = bob?.department?.head?.name
Same thing with Optional
monads:
Optional<String> name = bob .flatMap(Person::getDepartment) .map(Department::getHead) .flatMap(Person::getName);
AND POOR YOU IF YOU MIX UP map() WITH flatMap() JUST ONCE!!
“@EmrgencyKittens: cat in a box, in a box. http://t.co/ta976gqiQs” And I think flatMap
— Channing Walton (@channingwalton) March 23, 2014
Some people claim:
Using union types is like driving around in a brand new Ferrari with your mother-in-law in the passenger seat.
Sure. But I claim: Well done, Ceylon. Let’s hope we’ll get union types in Java, too, outside of catch blocks!
Reference: | Ceylon Might Just be the Only (JVM) Language that Got Nulls Right from our JCG partner Lukas Eder at the JAVA, SQL, AND JOOQ blog. |
It is definitely not fair title: at least Kotlin does null right ;)
Even with the very humble “might just be” self-deprecating title, this criticism wasn’t exactly unexpected ;)
Kotlin can still get it right by supporting union types.
They should be able to add type aliases to Java by allowing us to redeclare existing classes (that we don’t own) to implement additional interfaces (with default methods as necessary). A type alias for a union type could then be nothing but a marker interface. I don’t imagine this would be too difficult. Hasn’t Kotlin a similar mechanism?
Even PHP got NULLs right, then :)