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.Prism
s 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.