This is a proposal to replace the current exception mechanism in the base library with extensible exceptions.
It also reimplements the existing exceptions on top of extensible exceptions, for legacy applications.
Proposed deadline: 25th July.
What are extensible exceptions?
Simon's extensible extensions paper is very easy to read, and describes the problems and proposed solution very well:
I won't try to reproduce everything the paper says here, but here is the list of what we want extracted from it:
- A hierarchy of exception types, such that a particular catch can choose to catch only exceptions that belong to a particular subclass and re-throw all others.
- A way to add new exception types at any point in the hierarchy from library or program code.
- The boilerplate code required to add a new type to the exception hierarchy should be minimal.
- Exceptions should be thrown and caught using the same primitives, regardless of the types involved.
I heartily recommend having a read through of the paper.
Patches and examples
The patches are here:
Examples.hs, which gives some examples of using it.
The patches aren't polished; if this proposal is accepted then there is some more work to do, moving things around inside the base package to simplify the dependencies, and to maximise the amount of code that can be shared between all the impls. There's also some GHC-specific fiddling to be done, to make GHC.!TopHandler use the new exceptions. This can all be done without further library proposals, though.
Also, currently it derives Data.Typeable, which is unportable, but we can easily work around that. The only extensions that I don't think that we can do without are !ExistentialQuantification and Rank2Types. !DeriveDataTypeable makes the implementation easier, and !DeriveDataTypeable and !PatternSignatures make using it easier.
Library function differences
As far as the library functions are concerned, here are the main differences:
The old and new types for catch are:
Old: catch :: IO a -> (Exception -> IO a) -> IO a New: catch :: Exception e => IO a -> (e -> IO a) -> IO a
i.e. catch can now catch any type of exception; we don't have to force all the different types of extension into one fixed datatype.
All the other exception functions are similarly changed to handle any type of extension, e.g. we now have
try :: Exception e => IO a -> IO (Either e a)
Now that you can write handlers for different exception types, you might want to catch multiple different types at the same point. You can use catches for this. For example, the !OldException module needs to catch all the new exception types and put them into the old Exception type, so that the legacy handler can be run on them. It looks like this:
catch :: IO a -> (Exception -> IO a) -> IO a catch io handler = io `catches` [Handler (\e -> handler e), Handler (\exc -> handler (ArithException exc)), Handler (\exc -> handler (ArrayException exc)), ...]
where the first Handler deals with exceptions of type Exception, the second those of type !ArithException, and so on.
If you want to catch all exceptions, e.g. if you want to cleanup and rethrow the exception, or just print the exception at the top-level, you can use the new function catchAny:
catchAny :: IO a -> (forall e . Exception e => e -> IO a) -> IO a
You can happily write
`catchAny` \e -> print e
`catch` \e -> print e
would give you an ambiguous type variable error.
ignoreExceptions :: IO () -> IO ()
which can be used instead of try for things like
ignoreExceptions (hClose h)
(where we don't look at the result, so the exception type would be ambiguous if we used try). (I'm not sure if this is the best name for this function).
All the build failures I've seen with the new exceptions library have been cases where you need to change a "catch" to "catchAny", "try" to "ignoreExceptions", or occassionally a different function, e.g. "bracket" or "handle", is used to handle any extension, so adding a type signature involving the !SomeException type solves the problem.
The old interface is available in Control.!OldException. Currently it doesn't catch exceptions that don't fit into the old Exception type; we could catch them, show them and treat them as user errors, but then the exception has changed if it gets rethrown.