We've talked quite a lot about union types, and even seen some of their many applications, but one thing I didn't mention is that they can be used as a kind of checked exception facility. Consider the following method declaration:
shared String|Error format(String format, Object... args) { .... }
The return type of this method forces you to handle the possibility of an Error result, just like a checked exception does. You must either deal with the problem immediately in the calling code:
String formatEntry(Entry e) { local result = format("%s->%s", $e.key, $e.value); if (is String result) return result; } else { return "Invalid entry"; } }
Here the if/else performs the role of a try/catch. Ceylon's various static analysis facilities (in this case, the safe narrowing construct if (is ...) and definite return checking) conspire to force you to explicitly handle the possibility of Error.
Or you must propagate the possibility of error up the stack:
String|Error formatEntry(Entry e) { return format("%s->%s", $e.key, $e.value); }
Multiple exception types may be represented by including them all in the union, for example String|SomeError|OtherError, or by using a supertype, for example String|GenericError. Just like with real language-level support for checked exceptions.
Actually, Ceylon's handling of optional values, where the type X? is just a shortcut for X|Nothing can be viewed as really just a special case of this pattern. Error is just a null
which carries a little more information than null itself.
I suppose that this probably isn't a pattern we should very much encourage in Ceylon, in view of the nasty failure of checked exceptions in Java, but I can imagine it being occasionally useful. In particular, it might sometimes be useful to be able to provide some information about why a return value is null
.