exceptions-checked: Statically Checked Exceptions

This package provides an API to statically check exceptions at the type-level.
Think of it like checked exceptions in Java, but with better ergonomics. People
sometimes claim that checked exceptions are a failed experiment. This module is
an attempt to prove the contrary.
Though there are enough differences to warrant a separate package, this work is
heavily derived from Pepe Iborra's
control-monad-exception package and supporting paper
“Explicitly typed exceptions for Haskell”.
Some features include:
-
delegation of exception handling to the safe-exceptions
library (instead of Control.Exception) for improved handling of synchronous
versus asynchronous exceptions.
-
an ergonomic API including not just a basic throw and catch, but most
of the handling offered by safe-exceptions (catches, finally,
bracket, onException, and so forth).
Motivation
We generally want types to help us avoid defects, but what can we do about
types like IO that threaten to throw a myriad of exceptions? With types like
Maybe and Either, we can use the type-checker to assure that we've
exhaustively covered all cases with our pattern matching. But this can lead to
at least two problems:
A split world
To deal with the composition of Either and IO, people commonly use
ExceptT or MonadError, but now we have to think about handling errors
through two APIs: Control.Exception and Control.Monad.Except. Migrating
from thrown exceptions in IO to “error” data types in Either or ExceptT
can feel tedious.
Also, if we want to implement things like onFailure. Do we want to call our
action when there's an exception thrown? Or upon returning a Left? Or both?
We can certainly write functions for all possibilities, but it would be nicer
if we didn't have this complexity at all.
Non-extensible errors
We have further boilerplate with ExceptT and MonadError every time we
handle a possible error condition. We end up defining data types for every
combination of errors we might have at any given moment. For instance, if we
have an error type like
data MyError = NetworkFailed | MsgMalformed
and we're given a ExceptT MyError m a, what should we return if we handle the
network exception, but still have the possibility of a malformed message? It
seems we need a new data type, because pattern matching over MyError forces
us to deal with both cases.
Also, note that mtl's MonadError type class gets in the way because the
only way we can move from MonadError ErrorA to MonadError ErrorB is by
running a concrete transformer stack, which defeats the polymorphism we may
desire by using mtl, specifically not committing to any specific transformer
stack before we're ready.
Fans of lens may use Control.Lens.Prisms and make “Has” type classes,
maybe with Template Haskell using Control.Lens.TH.makeClassyPrisms, but even
this non-trivial solution doesn't really help us solve the problem we have of
needing to define data types for when conditions are handled within a program.
Otherwise, we can't discharge the “Has” constraint.
It would be nice if Haskell had row polymorphism, which would naturally solve
this problem. Still, we can employ heterogenous lists to represent an
extensible error type. A variety of libraries can assist with this, for
example haskus-utils-variant.
Building beyond heterogenous lists, we may end up with something like
extensible-effects, which maintains an expressive
type-level list of “effects,” one of which can capture which errors have been
thrown and which are not yet handled.
Putting aside debates about performance and lawfulness when considering
extensible-effects versus mtl, it seems clear that the extensible-effects
library pretty well provides us a library for extensible errors. Unfortunately,
we still have a split-world between errors in its Eff type and exceptions in
the underlying base (often IO). If we want to call things like finally or
onException, we have to use the Lift effect, which provides MonadBase,
MonadBaseControl, and MonadIO instances.
Our solution
Pepe Iborra's control-monad-exception package seems
to address the “split-world” problem, while also providing extensible errors.
With it, we can leave our exceptions within IO, and just annotate which
exceptions have been thrown and caught with a Throws constraint.
control-monad-exception wraps our computation with a Checked data type.
Some have tried to avoid this wrapping, but seem to
run into issues with less type inference,
limitations of the API, and possible idiosyncrasies with forkIO.
For that reason, in this package we stick with the basic approach of
control-monad-exception, but we have a different approach to the API design,
and also choose to delegate exception management calls to the safe-exception
package rather than base's Control.Exception.
Using the library candidates
This library is not yet officially released on Hackage, though
candidates are being published to Hackage.
Candidates in Hackage are not completely implemented, and there's not yet a
standard workflow for them. For now, we're just using them as a sanity check
of the upload and to review documentation. That said, we republish candidates
under the same version number, which mutates them.
Tags on GitHub won't change, and we won't force-push on either the “candidate”
or “release” branch (though “user/*” have no such guarantees). So we
recommend you get candidates directly from GitHub. This can be done with both
Cabal and Stack.
Pulling in candidates with Cabal
If you're using a recent release of Cabal, you can put a
source-repository-package stanza in your cabal.project file.
Tags of candidates can be found on GitHub. For instance, to use the
“candidate/0.0.1-rc1” candidate, you can include the following in
cabal.project:
source-repository-package
type: git
location: https://github.com/shajra/exceptions-checked
tag: candidate/0.0.1-rc1
You can then use the exceptions-checked package in your Cabal file as usual.
Pulling in candidates with Stack
Alternatively for Stack, to use the “candidate/0.0.1-rc1” candidate, you can
put the following in your stack.yaml file:
extra-deps:
- github: shajra/exceptions-checked
commit: candidate/0.0.1-rc1
You can then use the exceptions-checked package in your Cabal file as usual.