I love both, but Java's implementation of checked exceptions cause harder and harder problems, especially when combined with tools such as lambdas, since there's no way to generically compose or handle checked exceptions inside them.
Proper Either/Result would fix some of the exception problems with lambdas in Java, though.
It also allows the exceptional behavior to be defined and to be controlled by the developer, one thing that you don't really have today when throwing RuntimeExceptions with say Stream APIs.
Java's checked exceptions are "proper Either/Result." The problem is that they can be of a more complex types (union types) combined with subtyping. In other words, what you see with Either is not a result of it being written as a return type, but a result of it being a much simpler type than Java's exceptions.
The problem boils down to the fact that you can have a disjunction of any number of checked exception types (including zero). No other party of the type system allows disjunctions, so it causes a lot of problems. The checked exception is conceptually part of the return type, but is split out.
I wish Kotlin had, instead of ignoring the existence of checked exceptions, instead translated them into part of the return type. I use Kotlin a lot these days, and one annoyance for me is dealing with code that throws exceptions. They fixed the annoying "(almost) anything can be null" problem of java and replaced it with an equivalent problem. Why can't nullability and failure results both be part of the static type?
(The workaround it to manually use an Either type yourself, but it doesn't help you with calling anyone else's code, since virtually everything throws exceptions on failure.)
Yes, but they are the only part of Java's type system that allows sum types.
How do you declare a method that generically takes a function that in turn takes a K, returns a V, and can throw whatever checked exceptions it wants, and you'll rethrow them? Last I checked, this wasn't possible in Java.
If checked exceptions were instead replaced with sum type return values, then it becomes trivial.
@FunctionalInterface
interface ThrowingFunction<T, R, E extends Exception> {
R apply(T argument) throws E;
}
static <T, R, E extends Exception> R apply(T argument, ThrowingFunction<T, R, E> function) throws E {
return function.apply(argument);
}
But not catch and rethrow it, because throw and catch aren't generic, and the type parameter isn't reified. You can emulate reified generics with the usual trick of passing a Class object:
Right, it's horrid, and it only works for exactly one checked exception type. So there's no way to define a single `Stream.map` (for example) that takes a function that throws whatever number of checked exceptions and just propagates them.
> Java's implementation of checked exceptions looks pretty minimal and sound to me.
>
> How would you implement them?
Java's checked exceptions aren't "minimal" and are problematic because they behave unlike the rest of the types system. I'm not saying that they should have ditched checked exceptions and left it at that, though. What they should have done was make sum types a first-class part of the type system, and then checked exceptions become redundant.
In languages that actually have general sum types rather than special-casing the way Java's exceptions do, you can get multiple types of errors with an Either by making a union of the different types of errors. You can alternatively use an N-way sum type as your return value in place of the 2-way Either.
Kotlin has sealed classes, which can be used to create a sum type, and you can even do the same in Java with enough boilerplate, but in either of those languages you run into another problem if you try this: it doesn't interoperate well with the vast majority of existing library code that throws exceptions.
It's certainly fair to say I have no alternative recommendation. I enjoyed using them prior to Java 8, and they gave the guarantees I was looking for. They have just had a much harder time integrating with newer features than could be hoped for. I'm not sure how much of that is intrinsic to checked exceptions, and how much is intrinsic to Java's implementation.
One example of this: there is no way to encode the type of a checked exception in a generic. I would like to be able to express something like the following:
But there is no way to express that 'throwing E' part. The type of a checked exception is firmly embedded in the interface. So I can't supply, say, an IO method as a Function<S, T> parameter, since the IOException causes a mismatch, and I'd have to write a handler specifically for methods that throw IOException, another for those that throw TimeoutException, a third for those that throw IOException AND TimeoutException, etc; a fairly fruitless goal without automatic code generation.
Interesting. I tried this years ago, and it never worked. Maybe that's changed in recent versions? Arity versions are still more feasible at least, and many languages are willing to pay the cost for them.
AFAICR this has always worked. But you rapidly run into limitations - as i say in another comment, catch and throw are not themselves generic. Plus, none of the interfaces used in the streams API have exception parameters.
> Plus, none of the interfaces used in the streams API have exception parameters.
That's partly because there's another problem. Potentially, you could have streams of the form, say, `...map(A::foo).map(A::bar).map(A::baz).collect(...)`, and each of foo, bar, baz adds another exception type to the set that can be thrown by collect.
Result/Either-based streams have exactly the same problem. An exception-supporting stream API would solve it the same way: a function can either throw no exception, or an exception of the same type as thrown by a previous operation. You could have a .mapException operation which could change the exception type, like Result::map_err in Rust.
In practice, you would end up with the stream throwing an exception of some general type (in 95% of cases, IOException!), but you could still write specific catch blocks for each possible subtype.
Let me put it this way: the message I got from the language team is that the problem is certainly acknowledged, and various alternatives to dealing with it are known, but none of them has so far been shown the clearly preferable, "good" choice, so until one presents itself, the decision is to do nothing rather than add/remove features that could later have other negative consequences.
Maybe that was it. That means there's no way to generically remove or translate the exceptions, e.g. by wrapping it into a runtime exception, or into a specific Either value based on the exception type.
Yeah, I'm pretty sure there were some compiler bugs involving generic exceptions around 1.5 - 1.6. (I don't know if they ever got fixed -- I switched to Kotlin.)