Hacker News new | ask | show | jobs
by twic 2373 days ago
Absolutely wild how people are all in favour of Either/Result, but still look down on checked exceptions.
6 comments

Most languages with Either/Result have some form of monadic composition and type inference. This lets you basically ignore the error condition until you get to a level where it's appropriate to handle it, and the compiler will check the error types for consistency without you having to specify anything explicitly.

Either/Result has often been found to be untenable without these mechanisms, as well. Rust first added the try! macro and then the ? operator because it was so tedious to deal with raw Results otherwise.

This works for checked exceptions as well, just bubble it up to a place where you want to handle them by stating that your function might throw those exceptions.
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.
Java's implementation of checked exceptions looks pretty minimal and sound to me.

How would you implement them?

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.)

Checked exceptions are just sum types and you unpack it with a try catch block.
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.

You can do that and propagate the exception:

    @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:

    static <T, R, E extends Exception> R apply(T argument, ThrowingFunction<T, R, E> function, Class<E> witness) throws E {
        try {
            return function.apply(argument);
        } catch (Exception e) {
            throw witness.cast(e);
        }
    }
But this is pretty horrid.
Incorrect for two reasons:

1) They follow a different code path (which is what makes them a superior way to handle errors over return values).

2) They cannot be emulated by sum types when all you want to do is not handle the exception and let it bubble up the stack frames.

The problem is that try catch is so verbose
You might like Rust :)
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:

  interface ExceptionHandler<E extends Exception, S, T> {
    T wrap(
      Function<S, T, throwing E> fn,
      Function<E, T> exceptionMapper
    );
  }
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.
You can actually use a generic parameter in a throws clause, so you can write something like:

    interface FunctionThrows1<S, T, E1 extends Throwable> {
      T apply(S in) throws E1
    }
    interface FunctionThrows2<S, T, E1 extends Throwable, E2 extends Throwable> {
      T apply(S in) throws E1, E2
    }
    ...
But you still have to write one version of your method for each arity of throwing that you want to be able to wrap.
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.
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.)
The big difference between the two is that Either/Result are trivial to compose and otherwise deal with in higher-order functions. Java has a massive problem whereby any API that invokes a callback has to either require that said callback not throw anything outside of a given short list of exceptions; or else declare both itself and the callback as "throws Exception", and force the API caller to deal with that - even though the specific callback that caller is passing in might not be throwing anything, or might be throwing a very specific exception only.
Result / Either put the error condition into the return type, and thus compose better.

Checked exceptions were an excellent hack, but an ergonomic failure, despite the almost-pattern-matching of the `catch` clauses.

After Scala and Kotlin, it's so frustrating that Java almost had pattern matching but only for exceptions and nothing else.
iirc scala literally began as just java with pattern matching from odersky's frustration from building javac in raw java. pattern matching is well on its way in modern java, with switch expressions and instanceof binding in jdk14 preview and more general destructuring (further enhanced by records) and guards up next.

in reality though aside from the rock/hardplace of language conservatism/fanatical back-compat i have to imagine a real reason modern java has yet to fully embrace Optional/Either over null/exceptions is lack of value types - yeah escape analysis lets you ~mostly~ not have to worry about the IdentityObjects you're returning everywhere, until you hit something it can't/won't inline or try to stuff those Optionals on heap and wind up making our already painful pointer chasing situation ten times worse. value types are also well on their way via project valhalla but have yet to actually land.

maybe ironically one of the things i recently got bitten by was the fact that MethodHandle .invoke* methods throw Throwable when used directly in plain java - the very machinery that enables the efficient composition of these kinds of functional programming styles is gated behind having to catch literally anything in the language itself. i guess the recurring theme here is java putting the horse before the cart, and that's frankly my favorite thing about the ecosystem.

I've been reading articles like this for twenty years now, and i've never found one anything other than idiotic.
I loved checked exceptions too, but the ergonomics of Either<Error, Result> works much better if you're writing code in a functional style. I even wrote my own Either which was a fun exercise.