Safe Haskell | None |
---|---|
Language | Haskell2010 |
In Plucking Constraints, I described a technique where you can collect constraints and then pluck them off one-by-one. In order to pluck them off, you need a type class and a datatype that supports concretely matching one of the constraints and delegating the rest of them.
I hint that errors can be handled in this way, and this library
demonstrates the technique. I do not make any guarantee of
prodution-readiness or maintainability, and indeed this technique does
not work with mtl
's MonadError
, due to the functional dependency. So
this is primarily useful in a concrete ExceptT
monad or with Either
directly.
Synopsis
- throw :: ProjectError e' e => e -> Either e' x
- rethrow :: e -> Either e a
- throwT :: (Monad m, ProjectError e' e) => e -> ExceptT e' m a
- catch :: Either a b -> (a -> Either a' b) -> Either a' b
- catchOne :: Either (Either a b) c -> (a -> Either b c) -> Either b c
- catchT :: Monad m => ExceptT e m a -> (e -> ExceptT e' m a) -> ExceptT e' m a
- catchOneT :: Monad m => ExceptT (Either e e') m a -> (e -> ExceptT e' m a) -> ExceptT e' m a
- type family OneOf a as :: Constraint where ...
- class ProjectError large single where
- putError :: single -> large
Throwing Errors
throw :: ProjectError e' e => e -> Either e' x Source #
rethrow :: e -> Either e a Source #
When you're pattern matching on an unknown error, and you don't know
how to handle it, you'll probably want to rethrow
it.
handlingThree =catch
three $ \error -> case error ofLeft
A -> pure "It was an A."Right
other ->rethrow
other
In practice, you'll probably be using catchOne
, and this won't be
necessary.
Since: 0.0.0.0
Catching Errors
catch :: Either a b -> (a -> Either a' b) -> Either a' b Source #
This function can be used to handle the entirety of the error cases. Using this will require that you pattern match on the error, which can allow you to handle multiple cases.
Note that each different error case will live in a different "level" of
the Either
that it's contained in. So you'll need to nest the pattern
matching.
To give an example, let's consider the case presented in catchOne
- a function that throws three errors.
data A = A
three :: (OneOf
error [A, B, C]) => Either error String
We can handle the possibility of A
and B
at the same time:
handlingTwoErrors ::OneOf
error [C] =>Either
errorString
handlingTwoErrors = catch three $ \ error -> case error ofLeft
A ->pure
"It's an A"Right
(Left
B) ->pure
"It's a B"Right
(Right
other) ->rethrow
other
You will probably have an easier time using catchOne
sequentially,
which would look like this:
handlingTwoErrors ::OneOf
error [C] =>Either
errorString
handlingTwoErrors =catchOne
(catchOne
three $ \A ->pure
"It's an A") $ \B ->pure
"It's a B!"
TODO: Should this be the function that catch
refers to? Or should
catchOne
be the guided case, and this is catchAll
?
Since: 0.0.0.0
catchOne :: Either (Either a b) c -> (a -> Either b c) -> Either b c Source #
This function is useful to handle a single exception type among many. You pick which error you're going to handle by the handler function's input, and it magically rearranges everything else to 'just work' for you.
Let's say we have a program that throws three errors:
data A = A
three :: (OneOf
error [A, B, C]) => Either error String
At this point in our program logic, we can handle an A
just fine, but
we don't know how to deal with a B
or a C
just yet. So we'll use
catchOne
, and in the handler function, we'll pattern match on A
.
handleThree :: (OneOf
error [B, C]) => Either error String handleThree =catchOne
three $ \A -> pure "It was an A!"
If you want to rethrow the error with a different kind of exception, you can totally do that too.
data Z = Z aToZ :: (OneOf
error [B, C, Z]) => Either error String aToZ =catchOne
three $ \A ->throw
Z
Since: 0.0.0.0
catchOneT :: Monad m => ExceptT (Either e e') m a -> (e -> ExceptT e' m a) -> ExceptT e' m a Source #
Concise Type Signature Helper
type family OneOf a as :: Constraint where ... Source #
It's often annoying to write a bunch of ProjectError
annotations.
Consider the three
example:
three :: (ProjectError
e A,ProjectError
e B,ProjectError
e C) => Either e String three = doLeft
(putError
A)Left
(putError
B)Left
(putError
C)pure
"hello, world"
That constraint is so annoying and repetitive. So instead this is defined to allow you to write:
three :: OneOf
e [A, B, C] => Either e String
Since: 0.0.0.0
OneOf a (x ': xs) = (ProjectError a x, OneOf a xs) | |
OneOf a '[] = () |
The magical internal guts!
This type class has several instances that are well-documented and describe how the library works and why. If you're curious about the way this library works, read this up.
class ProjectError large single where Source #
This type class is used to create the delegated values. It's an internal implementation detail, and you should not write any instances of it.
The real meat is in the instance documentation, so make sure you check that out.
TODO: rename this class
Since: 0.0.0.0
putError :: single -> large Source #
The type class has a single function which is used to shove
a single
value into a large
value that contains it somehow. We
use this to tell GHC how to organize our Either
s so that
everything magically works out.
Since: 0.0.0.0
Instances
(TypeError (Text "No (remind me to write a better error message)") :: Constraint) => ProjectError a b Source # | Finally, we have an "error" instance. This one is here to blow up in the event that you try to trick GHC into thinking you've handled all the possible errors, when you've only really handled one. You shouldn't see this. Since: 0.0.0.0 |
Defined in Data.Either.Plucky | |
ProjectError a a Source # | This instance is the most base of base cases. A value may just be itself! This case can trigger whenever you only have a single constraint on a type. Let's consider the following code: data X = X value :: ( There's only a single constraint on the error type. If we want to, we
can write a specialized version of value' :: Now, there's no weird polymorphism or type variables, and we can just pattern match directly on the value. However, we can skip the manual specializing of the type, and pattern match directly on the value: main :: IO () main = do case value of Left X -> print "it was an X" Right () -> pure () Since: 0.0.0.0 |
Defined in Data.Either.Plucky | |
ProjectError b c => ProjectError (Either a b) c Source # | This is the "recursive case" in our plucking experiment. If you have
a Let's review the example from above, three :: ( How does this exactly work out?
Well, let's pick the three :: ( The "terminating" instance ( three :: ( Neat! Now, we can also partially specialize the three :: ( We can specialize the type of three
:: ( I specifically chose This specializes to
(if you feel tempted to march, please take a small break from reading these documentations). Likewise, we can further specialize the
Stitching that all together, we get: three :: ( There's one final expansion we can do here. Because there is only
a single constraint remaining, we can use the "identity" base case.
In that case, three :: Either (Either A (Either C B)) String three = do As with every other example, all of this "unwinding" is totally unnecessary for you, the library user, to ever do. You can pattern match directly on the totally polymorphic type to match on exceptions, and the errors you match on determine how GHC constructs the values for you. In practice, you'll use other functions to manipulate these exceptions. Since: 0.0.0.0 |
Defined in Data.Either.Plucky | |
ProjectError (Either a b) a Source # | This is another terminating case in data A = A data B = B data C = C three :: ( This is pretty silly, sure! But if you have a function which can throw
multiple error types, this is what you'll end up with. Let's suppose you
know how to handle the error three' :: ( Now, we can pattern match directly on the main :: IO () main = do case three of Left err -> case err of Left B -> print "It was B." Right other -> error "wasn't B!" Right msg -> putStrLn msg This pattern is a little boring, and we'll introduce some helper functions to match and fix it directly. Since: 0.0.0.0 |
Defined in Data.Either.Plucky |