interface Function<T,U,Err> {
U apply(T value) throws Err;
}
interface List<T> {
<U,Err> List<U> map(Function<T,U,Err> f) throws Err;
}Right, whoops. My bad!
> In any case, Java does not have any of those, and this is what makes checked exceptions impractical to work with
Absolutely. I was approaching it from the standpoint of "what would I change about Java's type system to make checked exceptions usable?".
> Rust has sum types but I don't think it has either inclusive unions or never types.
Rust is working on stabilizing a true never type[0], and until then you can emulate it with an empty enum.[1] There is also a RFC for unions, but that doesn't seem to have gone anywere.[2]
[0]: https://github.com/rust-lang/rust/issues/57012
[1]: Such as https://crates.io/crates/void which also provides helpers for safely coercing `Void` `Result`s
The point wasn't that these things couldn't be solved -- they could, at least in theory[0] -- but that the current practical limitations of Java[1] are such that the interaction of higher-order functions and checked exceptions make checked exceptions a really bad idea.
[0] At least I think so... given anonymous or adhoc sum types, etc.
[1] Interestingly, this is not a JVM-wide issue. It's just the Java compiler that imposes these restrictions, for example: You cannot "catch" a checked exception which hasn't been declared at least one method in the "try" scope. This makes sense at first glance ("cannot happen"), but means that re-wrapping by higher-order functions must entail extremely error-prone inspection of any exception by calling code, etc. etc. It's a huge mess.
This only works to the first order. If you have functions-that-call-functions-that-call-functions, you end up not being able to write(!) the correct throws clauses in Java.
It's not just subclassing.
It also breaks with higher-order functions, e.g. what is the throws clause of a map() method on lists? There's really no good answer in Java, so what people do in practice is to just re-wrap into a RuntimeException, but that leaves callers with a problem: Calling code cannot match on exception types using a catch clause on the checked exception -- it has to check for a RuntimeException with an embedded checked exception. This is disastrous for ergonomics and real-life use of checked exceptions.
The Result/Either method leads to its own ergonomic problems, but I think they could be solvable with support for row-types and (anonymous) union types. At least in Haskell, the Either method leads to a proliferation of FooError types whose only purpose is to wrap other error types. Well, either that or you accept that every little thing can return in a generic 'AppError'.