Hacker News new | ask | show | jobs
by chriswarbo 2105 days ago
There are three possibilities:

                         | Polymorphic exceptions | Monomorphic exceptions
    ---------------------+------------------------+------------------------
      Checked exceptions | Unsupported            | Java
    ---------------------+-------------------------------------------------
    Unchecked exceptions |                      Scala
If exceptions are unchecked then there is no difference between "inner" code and "outermost" code; the compiler cannot tell us that a handler is needed/missing. This is the case in Scala. The advantage is that we can compose code which throws and which doesn't throw (this is essentially dynamic typing for exceptions).

If exceptions are checked then there is a difference between "inner" code and "outermost" code: the inner code has 'throws Foo' annotations, the "outermost" code doesn't. The compiler will spot missing handlers (i.e. when our outermost code can throw). There are two ways this could be done:

If checked exceptions aren't polymorphic then we need to make multiple versions of higher-order functions, like List::map: one version which doesn't throw, one which can throw one exception, one which can throw two exceptions, etc. (these exceptions can be kept generic, but the number of them must be explicit). For example if we have a lambda which can throw KeyNotFound we can't use it with the standard List::map method, since that only accepts lambdas which don't throw. We could make an alternative method 'public List<B> mapE(FunctionE<A, B, E> f) throws E', but that wouldn't work for lambdas which can throw FileNotFound and PermissionDenied; we could write a 'public List<B> mapEE(FunctionEE<A, B, E1, E2> f) throws E1, E2', but that wouldn't work for three exceptions, and so on. AFAIK this is the current situation in Java.

If checked exceptions could be polymorphic, similar to row polymorphism ( https://en.wikipedia.org/wiki/Row_polymorphism ) or algebraic effect systems ( http://lambda-the-ultimate.org/taxonomy/term/35 ), then we would have the best of both worlds. In this setup the 'E' in an annotation like 'throws E' doesn't stand for a name, but for a set of names. Higher-order functions like 'map' can throw the same set of exceptions as the lambda they're given, and that set could have any size: if the lambda can't throw any exceptions then map's set of exceptions is empty; if it can throw five types of exception then map's set of exceptions contains those five; and so on. This is becomes even clearer for things like function composition:

    public Function<A, C> compose(Function<B, C> f, Function<A, B> g)
If 'f' can throw something from set E1 and 'g' can throw something from set E2, then 'compose(f, g)' can throw something from set E1∪E2. Likewise if something can throw E1 and we have handlers for E2, then the result can throw E1 \ E2.

AFAIK the JVM can't do this, nor can those languages which typically target it (Java, Scala, Kotlin, etc.; Idris has algebraic effects and it can run on the JVM, although it's not the standard target)

1 comments

How aren't Java exceptions already polymorphic? "catch (Exception e)" will also match subclasses of Exception, won't it? And in the composition example, the compiler would correctly see that f(g(...)) can throw any subclass of E1 or E2. The only problem admittedly is that there's poor support for checked exceptions when used with lambdas, but isn't that just a language problem and not a JVM issue?

Although it's not possible to infer the exceptions automatically you can write a function to compose functions with exceptions and it will be checked at compilation time like you'd expect: https://gist.github.com/shawnz/5e9a0d344a6a693b46c662c5c8124... (EDIT: Actually they can be inferred to some extent.. example updated)

NVM. I see you addressed the possibility of doing this already.

There are two ways we can make exceptions polymorphic:

- Subtype polymorphism lets us say things like 'throws Exception', 'catch (Exception e) {...}', etc. and this will work for any sub-class of Exception, e.g. 'FileNotFound'. This works by upcasting: essentially discarding some of the information about the type, so the intermediate code can rely on a smaller interface. If we try to downcast it later, we need to handle the possibility that it doesn't match; e.g. we can write 'catch (FileNotFound e) {...}', but that won't remove the 'throws Exception' annotation, since we haven't handled the other possibilities.

- Parametric polymorphism (AKA generics) lets us say things like 'throws E', where E can be instantiated to any specific class, e.g. 'FileNotFound'. This doesn't upcast: the full type information is propagated (but the generic steps aren't allowed to use it). We don't need to downcast: the type checker will instantiate the generic types to that specified by the source, and see if the destination type matches. If 'E' is instantiated to 'FileNotFound', and we write 'catch (FileNotFound e) {...}', then the annotation will be removed, since there's nothing else to handle.

Hopefully the problem with the generic approach is clear from your example: we have to re-implement things over and over for different numbers of exceptions ('FuncThrowingOneException', 'FuncThrowingTwoExceptions', etc.)

Thinking about it, the situation is similar to Haskell's "constraint kinds": Haskell can "constrain" types (i.e. require interfaces), e.g.

    showBoth :: (Show a, Show b) => a -> b -> String
    showBoth x y = show x ++ ", " ++ show y
This is roughly equivalent to the Java:

    String showBoth<A extends Show, B extends Show>(A x, B y) {
      return x.show() + ", " + y.show();
    }
The GHC compiler has an extension "constraint kinds", where the constraints are treated more like normal values (similar to exceptions). The interesting part for this scenario is that each constraint is treated as a single value, so something like "(Show a, Show b)" is a single (tuple) value. Yet the type checker is smart enough to look inside such tuples, e.g. it knows that "(Show a, Show b)" implies "Show a", etc. It also doesn't care about order, e.g. "(Show b, Show a)" will work just as well; or nesting, e.g. if "c1" is "(Foo a, Bar a)" and "c2" is "(Bar a, Baz c)" then "(c1, c2)" is equivalent to "(Foo a, Bar a, Baz c)".

Those are the sort of features that would make generic exceptions much nicer, since we could put 'throws E' on everything, and be able to instantiate E to a single exception (like "FooException"), or a tuple of multiple exceptions (e.g. "(FooException, BarException, BazException)"), or a tuple of no exceptions "()". There's probably a way to encode this already, but it would require manually packing, re-arranging and unpacking the exceptions at every use-site.