I was wondering if there is an elegant way to do non-monadic error handling in Haskell that is syntactically simpler than using plain Maybe
or Either
. What I wanted to deal with is non-IO exceptions such as in parsing, where you generate the exception yourself to let yourself know at a later point, e.g., something was wrong in the input string.
The reason I ask is that monads seem to be viral to me. If I wanted to use exception or exception-like mechanism to report non-critical error in pure functions, I can always use either
and do case
analysis on the result. Once I use a monad, it's cumbersome/not easy to extract the content of a monadic value and feed it to a function not using monadic values.
A deeper reason is that monads seem to be an overkill for many error-handling. One rationale for using monads as I learned is that monads allow us to thread through a state. But in the case of reporting an error, I don't see any need for threading states (except for the failure state, which I honestly don't know whether it's essential to use monads).
(
EDIT: as I just read, in a monad, each action can take advantage of results from the previous actions. But in reporting an error, it is often unnecessary to know the results of the previous actions. So there is a potential over-kill here for using monads. All that is needed in many cases is to abort and report failure on-site without knowing any prior state. Applicative
seems to be a less restrictive choice here to me.
In the specific example of parsing, are the execptions/errors we raise ourselves really effectual in nature? If not, is there something even weaker than Applicative
for to model error handling?
)
So, is there a weaker/more general paradigm than monads that can be used to model error-reporting? I am now reading Applicative
and trying to figure out if it's suitable. Just wanted to ask beforehand so that I don't miss the obvious.
A related question about this is whether there is a mechanism out there which simply enclose every basic type with,e.g., an Either String
. The reason I ask here is that all monads (or maybe functors) enclose a basic type with a type constructor. So if you want to change your non-exception-aware function to be exception aware, you go from, e.g.,
f:: a -> a -- non-exception-aware
to
f':: a -> m a -- exception-aware
But then, this change breaks functional compositions that would otherwise work in the non-exception case. While you could do
f (f x)
you can't do
f' (f' x)
because of the enclosure. A probably naive way to solve the composibilty issue is change f
to:
f'' :: m a -> m a
I wonder if there is an elegant way of making error handling/reporting work along this line?
Thanks.
-- Edit ---
Just to clarify the question, take an example from http://mvanier.livejournal.com/5103.html, to make a simple function like
g' i j k = i / k + j / k
capable of handling division by zero error, the current way is to break down the expression term-wise, and compute each term in a monadic action (somewhat like rewriting in assembly language):
g' :: Int -> Int -> Int -> Either ArithmeticError Int
g' i j k =
do q1 <- i `safe_divide` k
q2 <- j `safe_divide` k
return (q1 + q2)
Three actions would be necessary if (+)
can also incur an error. I think two reasons for this complexity in current approach are:
As the author of the tutorial pointed out, monads enforce a certain order of operations, which wasn't required in the original expression. That's where the non-monadic part of the question comes from (along with the "viral" feature of monads).
After the monadic computation, you don't have
Int
s, instead, you haveEither a Int
, which you cannot add directly. The boilerplate code would multiply rapidly when the express get more complex than addition of two terms. That's where the enclosing-everything-in-a-Either
part of the question comes from.
In your first example, you want to compose a function
f :: a -> m a
with itself. Let's pick a specifica
andm
for the sake of discussion:Int -> Maybe Int
.Composing functions that can have errors
Okay, so as you point out, you cannot just do
f (f x)
. Well, let's generalize this a little more tog (f x)
(let's say we're given ag :: Int -> Maybe String
to make things more concrete) and look at what you do need to do case-by-case:This is a bit verbose and, like you said, we would like to reduce the repetition. We can also notice a pattern:
Nothing
always goes toNothing
, and thex'
gets taken out ofJust x'
and given to the composition. Also, note that instead off x
, we could take anyMaybe Int
value to make things even more general. So let's also pull ourg
out into an argument, so we can give this function anyg
:With this helper function, we can rewrite our original
gComposeF
like this:This is getting pretty close to
g . f
, which is how you would compose those two functions if there wasn't theMaybe
discrepancy between them.Next, we can see that our
bindMaybe
function doesn't specifically needInt
orString
, so we can make this a little more useful:All we had to change, actually, was the type signature.
This already exists!
Now,
bindMaybe
actually already exists: it is the>>=
method from theMonad
type class!If we substitute
Maybe
form
(sinceMaybe
is an instance ofMonad
, we can do this) we get the same type asbindMaybe
:Let's take a look at the
Maybe
instance ofMonad
to be sure:Just like
bindMaybe
, except we also have an additional method that lets us put something into a "monadic context" (in this case, this just means wrapping it in aJust
). Our originalgComposeF
looks like this:There is also
=<<
, which is a flipped version of>>=
, that lets this look a little more like the normal composition version:There is also a builtin function for composing functions with types of the form
a -> m b
called<=<
:Now this really looks like function composition!
When we can simplify even more
As you mentioned in your question, using
do
notation to convert simple division function to one which properly handles errors is a bit harder to read and more verbose.Let's look at this a little more carefully, but let's start with a simpler problem (this is actually a simpler problem than the one we looked at in the first sections of this answer): We already have a function, say that multiplies by 10, and we want to compose it with a function that gives us a
Maybe Int
. We can immediately simplify this a little bit by saying that what we really want to do is take a "regular" function (such as ourmultiplyByTen :: Int -> Int
) and we want to give it aMaybe Int
(i.e., a value that won't exist in the case of an error). We want aMaybe Int
to come back too, because we want the error to propagate.For concreteness, we will say that we have some function
maybeCount :: String -> Maybe Int
(maybe divides 5 by the number times we use the word "compose" in theString
and rounds down. It doesn't really matter what it specifically though) and we want to applymultiplyByTen
to the result of that.We'll start with the same kind of case analysis:
We can, again, do a similar "pulling out" of
multiplyByTen
to generalize this further:These types also can be more general:
Note that we just needed to change the type signature, just like last time.
Our
countThenMultiply
can then be rewritten:This function also already exists!
This is
fmap
fromFunctor
!and, in fact, the definition of the
Maybe
instance is exactly the same as well. This lets us apply any "normal" function to aMaybe
value and get aMaybe
value back, with any failure automatically propagated.There is also a handy infix operator synonym for
fmap
:(<$>) = fmap
. This will come in handy later. This is what it would look like if we used this synonym:What if we have more
Maybes
?Maybe we have a "normal" function of multiple arguments that we need to apply to multiple
Maybe
values. As you have in your question, we could do this withMonad
anddo
notation if we were so inclined, but we don't actually need the full power ofMonad
. We need something in betweenFunctor
andMonad
.Let's look the division example you gave. We want to convert
g'
to use thesafeDivide :: Int -> Int -> Either ArithmeticError Int
. The "normal"g'
looks like this:What we would really like to do is something like this:
Well, we can get close with
Functor
:The type of this, by the way, is analogous to
Maybe (Int -> Int)
. TheEither ArithmeticError
part just tells us that our errors give us information in the form ofArithmeticError
values instead of only beingNothing
. It could help to mentally replaceEither ArithmeticError
withMaybe
for now.Well, this is sort of like what we want, but we need a way to apply the function "inside" the
Either ArithmeticError (Int -> Int)
toEither ArithmeticError Int
.Our case analysis would look like this:
(As a side note, the second
case
can be simplified withfmap
)If we have this function, then we can do this:
This still doesn't look great, but let's go with it for now.
It turns out
eitherApply
also already exists: it is(<*>)
fromApplicative
. If we use this, we can arrive at:You may remember from earlier that there is an infix synonym for
fmap
called<$>
. If we use that, the whole thing looks like:This looks strange at first, but you get used to it. You can think of
<$>
and<*>
as being "context sensitive whitespace." What I mean is, if we have some regular functionf :: String -> String -> Int
and we apply it to normalString
values we have:If we have two (for example)
Maybe String
values, we can applyf :: String -> String -> Int
, we can applyf
to both of them like this:The difference is that instead of whitespace, we add
<$>
and<*>
. This generalizes to more arguments in this way (givenf :: A -> B -> C -> D -> E
):A very important note
Note that none of the above code mentioned
Functor
,Applicative
, orMonad
. We just used their methods as though they are any other regular helper functions.The only difference is that these particular helper functions can work on many different types, but we don't even have to think about that if we don't want to. If we really want to, we can just think of
fmap
,<*>
,>>=
etc in terms of their specialized types, if we are using them on a specific type (which we are, in all of this).Such viral character is actually well-suited to exception handling, as it forces you to recognize your functions may fail and to deal with the failure cases.
You don't have to extract the value. Taking
Maybe
as a simple example, very often you can just write plain functions to deal with success cases, and then usefmap
to apply them to yourMaybe
values andmaybe
/fromMaybe
to deal with failures and eliminate theMaybe
wrapping.Maybe
is a monad, but that doesn't oblige you to use the monadic interface ordo
notation all the time. In general, there is no real opposition between "monadic" and "pure".That is just one of many use cases. The
Maybe
monad allows you to skip any remaining computations in a bind chain after failure. It does not thread any sort of state.You can certainly chain
Maybe
computations using theApplicative
instance.(*>)
is equivalent to(>>)
, and there is no equivalent to(>>=)
sinceApplicative
is less powerful thanMonad
. While it is generally a good thing not to use more power than you actually need, I am not sure if usingApplicative
is any simpler in the sense you aim at.You can write
f' <=< f' $ x
though:You may find this answer about
(>=>)
, and possibly the other discussions in that question, interesting.