Hacker News new | ask | show | jobs
by chriswarbo 2097 days ago
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.