3

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:

  1. 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).

  2. After the monadic computation, you don't have Ints, instead, you have Either 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.

thor
  • 21,418
  • 31
  • 87
  • 173
  • ... But `Maybe` and `Either` _are_ monads and are elegant. It is hard to answer the question when it makes so many contentious statements. Perhaps you are looking for monoids? – Thomas M. DuBuisson Apr 21 '14 at 03:29
  • @ThomasM.DuBuisson I totally agree with the elegance of using `Maybe` and `Either` as monads compared to using them with case statements. I was wondering if there are ways more elegant than monads in handling, e.g., "logical" exceptions that we raise ourselves (parsing and division by zeros). – thor Apr 21 '14 at 03:38
  • Using monads forces breaking down expressions into assembly-like actions as in the examples I cited, among other things. I suspect that it is because monads impose too much. I do realize I asked a lot. But this issue with incurring monads in certain simple error-handling has bugged for quite a while. Not even sure myself how to formulate the question. I am looking into `Applicative` right now as someone seem to have done error handling in an applicative style in the 1980s. Any specific example/references for `monoid`s for error-handling? – thor Apr 21 '14 at 03:39
  • There is nothing "viral" about monads. I feel like you might be confusing the general concept of monads with the specific monad instance `IO`. The fact that you can't "get out" of `IO` has nothing to do with it being a monad. – David Young Apr 21 '14 at 17:22
  • I think you are right here about `IO`. I guess I was also think about the fact that using the `do` notation changes the programming significantly as compared to evaluating functions. Some did say that monads are re-invention of imperative?? programming in the Haskell world. And this mixing of two programming styles seems to happen as soon as you begin to involve monads. – thor Apr 21 '14 at 20:11
  • Maybe if a weaker form than monads suffices for reporting errors, we can have less imperative styles? – thor Apr 21 '14 at 20:16
  • @TingL: Actually, `do` notation gets turned into calls to `>>=` and `>>` by the compiler (I describe this translation here http://stackoverflow.com/questions/22964750/are-there-ways-to-call-two-functions-one-just-after-another-in-purely-function/22974692#22974692), so `do` is exactly equivalent to using `>>=` and `>>`. Most monads are not really "imperative," actually. `Maybe` and `Either` can be thought of to represent code that may fail, the list monad can be thought of as a nondeterministic computation and the function monad can be thought of as accessing a "shared environment." – David Young Apr 22 '14 at 00:20
  • @TingL The weaker form you are looking for is `Applicative`. `Applicative` is in between `Functor` and `Monad`. – David Young Apr 22 '14 at 00:22

2 Answers2

9

In your first example, you want to compose a function f :: a -> m a with itself. Let's pick a specific a and m 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 to g (f x) (let's say we're given a g :: Int -> Maybe String to make things more concrete) and look at what you do need to do case-by-case:

f :: Int -> Maybe Int
f = ...

g :: Int -> Maybe String
g = ...

gComposeF :: Int -> Maybe String
gComposeF x =
  case f x of           -- The f call on the inside
    Nothing -> Nothing
    Just x' -> g x'     -- The g call on the outside

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 to Nothing, and the x' gets taken out of Just x' and given to the composition. Also, note that instead of f x, we could take any Maybe Int value to make things even more general. So let's also pull our g out into an argument, so we can give this function any g:

bindMaybe :: Maybe Int -> (Int -> Maybe String) -> Maybe String
bindMaybe Nothing   g = Nothing
bindMaybe (Just x') g = g x'

With this helper function, we can rewrite our original gComposeF like this:

gComposeF :: Int -> Maybe String
gComposeF x = bindMaybe (f x) g

This is getting pretty close to g . f, which is how you would compose those two functions if there wasn't the Maybe discrepancy between them.

Next, we can see that our bindMaybe function doesn't specifically need Int or String, so we can make this a little more useful:

bindMaybe :: Maybe a -> (a -> Maybe b) -> Maybe b
bindMaybe Nothing   g = Nothing
bindMaybe (Just x') g = g x'

All we had to change, actually, was the type signature.

This already exists!

Now, bindMaybe actually already exists: it is the >>= method from the Monad type class!

(>>=) :: Monad m => m a -> (a -> m b) -> m b

If we substitute Maybe for m (since Maybe is an instance of Monad, we can do this) we get the same type as bindMaybe:

(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b

Let's take a look at the Maybe instance of Monad to be sure:

instance Monad Maybe where
  return x      = Just x
  Nothing >>= f = Nothing
  Just x  >>= f = f x

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 a Just). Our original gComposeF looks like this:

gComposeF x = f x >>= g

There is also =<<, which is a flipped version of >>=, that lets this look a little more like the normal composition version:

gComposeF x = g =<< f x

There is also a builtin function for composing functions with types of the form a -> m b called <=<:

(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> a -> m c

-- Specialized to Maybe, we get:
(<=<) :: (b -> Maybe c) -> (a -> Maybe b) -> a -> Maybe c

Now this really looks like function composition!

gComposeF = g <=< f  -- This is very similar to g . f, which is how we "normally" compose functions

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 our multiplyByTen :: Int -> Int) and we want to give it a Maybe Int (i.e., a value that won't exist in the case of an error). We want a Maybe 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 the String and rounds down. It doesn't really matter what it specifically though) and we want to apply multiplyByTen to the result of that.

We'll start with the same kind of case analysis:

multiplyByTen :: Int -> Int
multiplyByTen x = x * 10

maybeCount :: String -> Maybe Int
maybeCount = ...

countThenMultiply :: String -> Maybe Int
countThenMultiply str =
  case maybeCount str of
    Nothing -> Nothing
    Just x  -> multiplyByTen x

We can, again, do a similar "pulling out" of multiplyByTen to generalize this further:

overMaybe :: (Int -> Int) -> Maybe Int -> Maybe Int
overMaybe f mstr =
  case mstr of
    Nothing -> Nothing
    Just x  -> f x

These types also can be more general:

overMaybe :: (a -> b) -> Maybe a -> Maybe b

Note that we just needed to change the type signature, just like last time.

Our countThenMultiply can then be rewritten:

countThenMultiply str = overMaybe multiplyByTen (maybeCount str)

This function also already exists!

This is fmap from Functor!

fmap :: Functor f => (a -> b) -> f a -> f b

-- Specializing f to Maybe:
fmap :: (a -> b) -> Maybe a -> Maybe b

and, in fact, the definition of the Maybe instance is exactly the same as well. This lets us apply any "normal" function to a Maybe value and get a Maybe 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:

countThenMultiply str = multiplyByTen <$> maybeCount str

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 with Monad and do notation if we were so inclined, but we don't actually need the full power of Monad. We need something in between Functor and Monad.

Let's look the division example you gave. We want to convert g' to use the safeDivide :: Int -> Int -> Either ArithmeticError Int. The "normal" g' looks like this:

g' i j k = i / k + j / k

What we would really like to do is something like this:

g' i j k = (safeDivide i k) + (safeDivide j k)

Well, we can get close with Functor:

fmap (+) (safeDivide i k)    :: Either ArithmeticError (Int -> Int)

The type of this, by the way, is analogous to Maybe (Int -> Int). The Either ArithmeticError part just tells us that our errors give us information in the form of ArithmeticError values instead of only being Nothing. It could help to mentally replace Either ArithmeticError with Maybe 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) to Either ArithmeticError Int.

Our case analysis would look like this:

eitherApply :: Either ArithmeticError (Int -> Int) -> Either ArithmeticError Int -> Either ArithmeticError Int
eitherApply ef ex =
  case ef of
    Left err -> Left err
    Right f  ->
      case ex of
        Left err' -> Left err'
        Right x   -> Right (f x)

(As a side note, the second case can be simplified with fmap)

If we have this function, then we can do this:

g' i j k = eitherApply (fmap (+) (safeDivide i k)) (safeDivide j k)

This still doesn't look great, but let's go with it for now.

It turns out eitherApply also already exists: it is (<*>) from Applicative. If we use this, we can arrive at:

g' i j k = (<*>) (fmap (+) (safeDivide i k)) (safeDivide j k)

-- This is the same as
g' i j k = fmap (+) (safeDivide i k) <*> safeDivide j k

You may remember from earlier that there is an infix synonym for fmap called <$>. If we use that, the whole thing looks like:

g' i j k = (+) <$> safeDivide i k <*> safeDivide j k

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 function f :: String -> String -> Int and we apply it to normal String values we have:

firstString, secondString :: String

result :: Int
result = f firstString secondString

If we have two (for example) Maybe String values, we can apply f :: String -> String -> Int, we can apply f to both of them like this:

firstString', secondString' :: Maybe String

result :: Maybe Int
result = f <$> firstString' <*> secondString'

The difference is that instead of whitespace, we add <$> and <*>. This generalizes to more arguments in this way (given f :: A -> B -> C -> D -> E):

-- When we apply normal values (x :: A, y :: B, z :: C, w :: D):
result :: E
result = f x y z w

-- When we apply values that have an Applicative instance, for example x' :: Maybe A, y' :: Maybe B, z' :: Maybe C, w' :: Maybe D:
result' :: Maybe E
result' = f <$> x' <*> y' <*> z' <*> w'

A very important note

Note that none of the above code mentioned Functor, Applicative, or Monad. 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).

David Young
  • 10,713
  • 2
  • 33
  • 47
  • Thanks a lot for the detailed answer. It's really helpful. One additional question. Suppose I want to do 'safeDivde 1 (safeDivide 1 (... (safeDivide 1 x)..))' an arbitrary number of times (as e.g. in Newton's methods), how would I use Control.Applicative to express it? It seems that using <$> and <*> will result in something wrapped in an indefinite number of 'Either ArithmeticError' s. – thor Apr 25 '14 at 07:31
  • @TingL: I would probably use `foldrM` from `Data.Foldable` for this (`foldrM` is like `foldr` but it works in a monadic context): `flipFlop nestingLevel n = foldrM safeDivide n (replicate nestingLevel 1)`. The type signature for `foldrM` looks a little weird, but it helps to keep in mind that the `Traversable` instance we're using is `[]`, so you can replace the `t a` with `[a]`. – David Young Apr 25 '14 at 22:31
  • This is pretty similar to the non-monadic equivalent: `flipFlop nestingLevel n = foldr (/) n (replicate nestingLevel 1)`. Also, this is could be an example of when the extra abstraction level of type classes can be useful: this code works even if you change the `Monad` that `safeDivide` uses (maybe you decide you want `Maybe Int` instead of `Either ArithmeticError Int` at some point). – David Young Apr 25 '14 at 22:32
  • Oh, that should say "the `Foldable` instance" not "the `Traversable` instance" – David Young Apr 25 '14 at 22:52
  • Here's an equivalent definition that makes the monadic binding more apparent: `flipFlop 0 n = return n; flipFlop nestingLevel n = safeDivide 1 =<< flipFlop (nestingLevel - 1) n` (it doesn't use any lists so it is a bit structurally different. It performs the same function though). – David Young Apr 25 '14 at 23:13
5

The reason I ask is that monads seem to be viral to me.

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.

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.

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 use fmap to apply them to your Maybe values and maybe/fromMaybe to deal with failures and eliminate the Maybe wrapping. Maybe is a monad, but that doesn't oblige you to use the monadic interface or do notation all the time. In general, there is no real opposition between "monadic" and "pure".

One rationale for using monads as I learned is that monads allow us to thread through a state.

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.

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.

You can certainly chain Maybe computations using the Applicative instance. (*>) is equivalent to (>>), and there is no equivalent to (>>=) since Applicative is less powerful than Monad. While it is generally a good thing not to use more power than you actually need, I am not sure if using Applicative is any simpler in the sense you aim at.

While you could do f (f x) you can't do f' (f' x)

You can write f' <=< f' $ x though:

(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> a -> m c

You may find this answer about (>=>), and possibly the other discussions in that question, interesting.

Community
  • 1
  • 1
duplode
  • 33,731
  • 7
  • 79
  • 150
  • Thanks for your input. I will read (>=>) as you pointed out. On the other hand, while I recognize the necessity of the viral feature of monads as in handling IO execptions, I still believe it's nice to have something non-viral in error handling other occasions as in OP where we could have avoided monads by using plain `Maybe` and `Either`. My first point is whether there is a way to do the non-viral error-handling in an elegant manner. – thor Apr 21 '14 at 01:44
  • @TingL In a sense, plain `Maybe` is also viral in the sense that you need `fmap`, `(>>=)` and so forth to deal with the wrapped values until you eliminate the `Maybe`. Perhaps I am missing your point though. What would non-viral error handling look like? Do you have any specific situations in mind not involving `Maybe` or `Either`? – duplode Apr 21 '14 at 02:01
  • I don't want to eliminate `Either`, I want to find a more elegant way of using `Either` than monads, if possible at all. – thor Apr 21 '14 at 02:07
  • I was reading a tutorial about writing a Lisp interpreter in 48 hours in Haskell a few years ago. Everything is critical clear expressions, until the point when you reach error handling where the author begun to use either an IOException or another monad, which made it necessary to overhaul the entire type system from square one. I was wondering from then on: is this really necessary? If you are parsing something and can routinely go wrong, why enclose everything relevant in `Either`? – thor Apr 21 '14 at 02:11
  • I do realize that I can use `Either` or `Maybe` and use `case` analysis to avoid introducing monads into the type system. But it's obvious too much boilerplate code and unreadable. – thor Apr 21 '14 at 02:14
  • @TingL As I see it, the alternatives boil down to (1) not expressing the possibility of failure in the types (i.e. conventional exception throwing, which opens a can of worms and, in Haskell, requires `IO`); (2) using union types and case analysis (e.g. `Maybe` and `Either`); and (3) using record types and flag checking (which is far messier). That being so, the `Maybe` monad can be seen as a way to reduce the boilerplate for doing (2). The sequencing isn't gratuitous either; it allows us to capture the control flow aspect of exception handling. – duplode Apr 21 '14 at 03:07
  • @TingL "If you are parsing something and can routinely go wrong, why enclose everything relevant in `Either`?" I suggest the opposite argument: because it goes wrong routinely we want to be explicit about failure. In addition, the point of `Either` is not just indicating failure, but also transmitting error information and allowing for recovery in a way that is principled and, as far as possible, convenient. – duplode Apr 21 '14 at 03:15
  • @Ting L: Actually, the functor, applicative and monad instances exist to remove the very boilerplate that you're referring to. Also, using the methods of the monad typeclass for a specific monad doesn't introduce monads. It's just like using any other function that uses Either or Maybe (likewise with any other type class like functor and applicative). – David Young Apr 21 '14 at 17:28
  • You wouldn't think of using fmap on a list as introducing functor into the type. It's really no different than that. – David Young Apr 21 '14 at 17:34