/haskell-exceptions-checked

Statically Checked Exceptions for Haskell

Primary LanguageHaskellBSD 3-Clause "New" or "Revised" LicenseBSD-3-Clause

exceptions-checked: Statically Checked Exceptions

Release Branch Build Status Candidate Branch Build Status

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 view of errors as plain values versus exceptions in IO

  • non-extensible error types.

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.