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:
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!
}
And you can emulate union types with generics, which don’t support union types but intersection types in Java.
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!!
Some people claim
Using union types is like driving around in a brand new Ferrari with your mother-in-law in the passenger seat.
― by Elvira
Sure. But I claim: Well done, Ceylon. Let’s hope we’ll get union types in Java, too, outside of catch blocks!
Further reading
Liked this article? How about: