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
.
Not really happy with this one: If I don't check the result because it's a void or an error, I will not be force to handle the Error. It's the return of the infamous File.delete() for which nobody checks the result and that failed without any warning.
Plus if I have to call 3 methods that may return an Error, I will have to handle the if (... is Error) return for each one. I won't be able to call a.foo().bar() if foo may return an Error.
That's true. This definitely only works out for non-side-effecty methods.
Also true.
Anyway, I'm not proposing a return to checked exceptions. I regard them as a very interesting experiment in Java that ultimately turned out to be a failure. But I do think there is some value here as a .
P.S. Thanks for the excellent comment. I'm really, really stoked about the super-high-quality comments everyone has been posting here on Ceylon-related postings.
Java checked exceptions sometimes required unnecessary work from me. But the total of all such time I spent unnecessarily in the last 10 years is less than the time I spend on any single refactoring of unchecked exceptions when those unchecked exceptions are actually handled.
@Adrien: if
can return either or then it is dangerous to write and so you should not. If both alternatives and support , then type checking should discover that and all will be ok. So this is not a real issue.I cannot recommend union types highly enough. They are a great tool. The only thing I am concerned about a little bit ni Ceylon is the strange way in which
statements replace what would be the natural way to destruct union types, namely with a . I would imagine that instead of writing something likeif (is Rabbit x) { R(x) } else if (is Cow x) { C(x) } else { D(x); }it would be far more readable to have a
(not to mention that you make it easy on the compiler to analyze whether all cases are there):switch (x) { case Rabbit: R(x); case Cow: C(x); default: D(x) }And since nobody uses the (now broken)
in Java, it would be ok to chnage the meaning of it.P.S. You have insane timeout setting on this web site. While I typed my comment the page told me I . Why am I not allowed to think about what I want to say?
Oops, I messed up the formatting (did not see button). Someone please fix those code-block to inline code. Sorry!
You can use either if or switch, see here.
Pity
I know that this is the all pervasive view with regards to checked exceptions, but I also think it is incorrect. In my opinion the mistake in Java with regards Exceptions was to allow an Exception that wasn't checked. The only case for that I can see is out of memory, because you can't actually deal with it. But really what is the point of an exception that isn't checked?
Pretty much every hard to track down bug I've ever had to fix in Java has been a consequence of someone using a runtime exception instead of a checked exception (usually some twit catching a checked exception, throwing it away, and throwing a runtime exception which is than not dealt with. or Null Pointer of course).
Of course it may be that the view that Checked Exceptions are a failure is now so widely held you have to drop them to get language acceptance. But if I could have one wish it would be to look at the problem again, and see if you can invent something which makes dealing with Exceptions easier.
Well, I guess I think that most good examples of exceptions are of this kind of nature. Their purpose is to represent conditions that can't be resolved locally, or at least probably can't be resolved localling. You might be able to deal with them eventually, but you can't deal with them here.
To give you a convenient way to handle an error condition by totally stopping what you're doing and recovering in a completely different part of your program. Arguably, this can make your program more maintainable by eliminating dependencies between code that deals with different concerns.
Exceptions are a convenience feature that do weaken some of the guarantees that the static type system provides (i.e. the promise that a function returns a String is a stronger guarantee than that it either returns a String or throws an unchecked exception).
And Java does have a couple of examples of exceptions which in my view significantly weaken the static type system. I'm looking at you, NullPointerException, ArrayIndexOutOfBoundsException, and ClassCastException. I believe there are strong reasons to not model these conditions and others like them as exceptions, and Ceylon goes far out of its way to avoid using exceptions at all in these cases. I think that ideally, reusable data structures should never throw exceptions. Rather, they should have operations which return a an optional type where the typesafe null represents failure. This wouldn't work in Java because null is untypesafe and can easily result in an unchecked exception further down the line. But it does work in Ceylon.
But on the other hand, conditions like failure to communicate with a database, or write to a file - which represent an infrastructural failures but actually occur deep within business code - seem like very reasonable usecases for exceptions.
Checked exceptions are a special case of effect typing. Indeed, I think you could even reasonably use them to model other kinds of effects. For example, you could give all operations that have a side effect upon the file system a Haskell-style IO effect by simply declaring them throws IO, even if nothing ever actually throws or catches this exception type. You could even force people to declare non-pure functions by requiring that every operation that sets a simple attribute be declared to throws Impure, even though we know that setting a simple attribute is an operation that succeeds. (To get this idea to work out you would have to add some restriction that effect types like IO and Impure can't appear in a catch clause.)
Well, I think this kind of effect typing is a really interesting idea, but in practice it simply turns out too inconvenient. It's OK for some library developers, perhaps, but for application developers it becomes an irritating distraction. What ends up happening is that an application ends up splattering all its code with throws IO|Impure.
And I think the same is true of exceptions like failure to access a database, etc, which in fact at some level are really just special cases of or variations upon the IO effect.
Indeed, you hear the same complaint about effect typing:
Thanks for the response Gavin. I agree with your point re convenience. Indeed another I've seen endlessly in Java code is a list of empty catch blocks - the sort of thing or a method that says - lots of horrible workarounds. I've also seen an argument that says if you update a library and it throws a new checked exception than lots of client code suddenly breaks. So a sort of versioning issue, I guess. Though I have to say in practice that in my experience it turns out to be a non-issue (if an update to a library results in new error conditions then my client code has to be updated anyway, surely). But are these really problems that are too hard to solve? Or are you saying that you just don't think solving them is worth the effort?
Right, I almost wrote that but then decided that this would be a vague statement that was easy to misinterpret. ;-)
Depends, depends. Where in the client code are error conditions being handled:
And does the library developer know where?
I usually handle most real exceptions (let's keep thinking of things like failures to communicate with the database, not programmer errors like NPE or AIOOBE that result from holes in the static typing of the language) in a single global exception handler. Libraries need to accommodate that kind of design, by not forcing me to add throws clauses all the way through my business code.
I'm saying that if there is a solution that has the benefits of effect typing without the inconvenience of effect typing, then I don't know what it would be and can't imagine what it would possibly look like. :-)