Is Alternative a Wrong Abstraction for Handling Failures?

Posted on February 13, 2021 by Robert Peszek
Last Modified on February 17, 2021
Revision History:
  • (2021.02.13) Edited Pessimistic Instances top section
  • (2021.02.14) Intro adds a clarification paragraph linking failures to instances (prompted by reddit), Nutshell clearly lists goals
  • (2021.02.15-16) Added Reader's Response section
  • (2021.02.16) Laws: clarified some text
  • (2021.02.17) Rethinking section reworded a little

subtitle: A Constructive Criticism Pessimism about the Alternative Typeclass

Code for this project can be found in my experiments repo (alternative folder).
This is my second post dedicated to the error information loss in Haskell (the first was about Maybe Overuse).

Nutshell

Alternative is a popular functional programming concept and the name of a frequently used Haskell typeclass. Alternative helps in writing elegant, concise code. Alternative instances are also known for producing confusing errors. In this post, we do a deep dive into the alternative thinking only about the errors.

The goals for this post are:

  • discuss how Alternative laws impact the instance ability to keep error information
  • provide examples of how some Alternative instances can get programmers in trouble
  • show a ‘blueprint’ Either [e] ([e],_) instance with a strong ability to preserve error information. This blueprint can be extended to a transformer, parser, etc.
  • very briefly discuss extending or rethinking the Alternative typeclass itself

I realized that there is an interesting connection between many Alternative instances and optimism:
Thinking about the glass being half empty or half full, look at this computation: a <|> b and assume that a fails and b succeeds.
A half empty glass makes us think about the failure of a:
Why did a fail?
Would it not be better if some a failures caused the whole computation to fail?…
A half full glass makes us ignore the failure and focus on b… this is exactly the semantics of <|>.
A half full glass is not what you always want. This post looks at the alternative, its laws, and its instances from the “pessimistic” point of view. In this post pessimism is defined as, simply, thinking about the errors.

This is a long post. You may prefer to pick a section you consider interesting and backtrack from it. The information is largely self-contained (except for referring to the laws).

Implementing good error messages is not trivial. This post should not be viewed as criticism but as a challenge.

I am using the term error colloquially, the correct term is exception. Exception information loss just does not have a ring to it.

Pessimist’s Intro to Alternative

class Applicative f => Alternative f where
  empty :: f a
  (<|>) :: f a -> f a -> f a
  some :: f a -> f [a] -- optional
  many :: f a -> f [a] -- optional

The typeclass does not specify the semantics of empty and <|> other than their monoidal nature.
However, many instances link the empty and <|> semantics to computation failures.

Optimist, First Look:

  • empty typically represents a failed computation
  • (<|>) combines 2 alternatives returning one that is successful. The most common approach is to return the left-to-right first success. This is called left-bias, I like to think about it as right-catch semantics.
  • some and many run the computation until first failure and return the successful results, some expects at least one success, otherwise it will fail. some and many are a nod towards parsers or other computations that change state. some and many are likely to yield bottom (e.g. many (Just 1) does not terminate).

Alternative is the Monoid for the * -> * types, empty representing mempty and <|> representing mappend. This equivalence is “witnessed” by the Data.Monoid.Alt monoid instance. The left-bias semantics is equivalent to the Data.Monoid.First monoid.

The definition of Alternative begs this question: Why the Applicative superclass? As far as I know this is because the intended use of empty and <|> is in the applicative context. More on this later.

As we know, MonadPlus provides a similar semantics for monads. Alternative and MonadPlus are most commonly used with parsers. You are likely to use it with aeson, parsec / megaparsec, attoparsec, etc.
In this post the focus is the Alternative with the typical right-catching, left-biased <|> and the examples typically use attoparsec.

Pessimist, First Look:

  • empty does not accept any error information. It represents a failure of some unknown reason.
    I consider this problematic and an oversimplification.
    Unless we introduce a zero-information (let me call it noOp) failure, this probably will bite.
    I will leave it to you to ponder philosophical questions about noOp (e.g. Left []) failure.
    What does: nothing went wrong but the computation failed mean?
  • (<|>) semantics is unclear about error information.
  • some and many provide no error information about the failure that ended the production of the result list. I will not discuss some or many in this post.

A somewhat popular behavior is: If all alternatives fail, then the error information comes from the last tried computation. Examples of alternatives that behave this way are: attoparsec Parser, aeson Parser, IO.

I am not that deeply familiar with GHC internals. However, as a black box, the GHC compiler often behaves in a very similar way. For example, GHC message could indicate a problem with unifying types; it may suggest that my function needs to be applied to more arguments; … while the real issue is that I am missing a typeclass instance somewhere or something else is happening that is completely not related to the error message. Generating useful compiler messages must be very hard and this is not a criticism of GHC but a familiar UX example.
From time to time, GHC will throw Haskell developers for a loop.

Side-Note: parsec and megaparsec packages implemented sophisticated ways to provide better error messages by looking at things like longest parsed path. Lack of backtracking is what makes the (maga)parsec Parser not a fully lawful Alternative/MonadPlus (violating right zero). The megaparsec haddock suggest adding try to improve the lawfulness. Adding try can mess up error messages. There appears to be an interesting pragmatic trade-off: good error messages vs more lawful alternative behavior.
A great, related, reading is: Parsec: “try a <|> b” considered harmful.

A random advice from a discussion about attoparsec errors:

The trick is to use the parsers library, which lets you switch out parsing backends. You can prototype with the trifecta library (which has good error messages) and then switch to attoparsec when you’re done

(I assume that the author was thinking about parsing something, like a standard protocol, where user input cannot cause errors.)
So, this is clearly a bit of a mess and we are looking for crazy workarounds. I will delve deeper into alternative error outputs by looking at the alternative laws next.

Alternative Laws, Pessimistically

The required laws (copied from Typeclassopedia, see also Haskell wikibooks) are:

empty <|> u  =  u    -- (1)
u <|> empty  =  u    -- (2)
u <|> (v <|> w)  =  (u <|> v) <|> w  -- (3)

Note that these laws do not link Applicative and Alternative in any way. That happens in the following, optional, set of laws:

f <*> empty = empty                        -- (4) Rigth Zero
(a <|> b) <*> c = (a <*> c) <|> (b <*> c)  -- (5) Left Distribution
a <*> (b <|> c) = (a <*> b) <|> (a <*> c)  -- (6) Right Distribution
(pure a) <|> x = pure a                    -- (7) Left catch

For example, when writing a parser you may decide to use one of these approaches:

p1 = Employee <$> employeeIdParser <*> (nameParser1 <|> nameParser2)
p2 = (Employee <$> employeeIdParser <*> nameParser1) <|> (Employee <$> employeeIdParser <*> nameParser2)

it is good to know that these approaches are equivalent.

The laws are strong enough to restrict what failures can be (this is not necessarily a bad thing). For example, (1) and (4) prevent expressing the concept of a critical failure. A sane definition would be: f is a critical failure if f <|> a = f and f <*> a = f for any a.
empty cannot represent a critical failure because of (1)
non-empty cannot represent a critical failure because of (4).
Critical failures are simply not what Alternative is about and that is OK.

Pessimist’s Concerns:

  • empty typically represents a failure. (4) is problematic if you want to have other possible failures (e.g. failures with different error messages):
    otherFailure <*> empty is likely to be otherFailure not empty.
  • (1 - 3) force a monoidal structure on the failures themselves (under a reasonable assumption that empty is a failure and alternating two failures produces a failure). This is a good property but instances often ignore it. A semigroup structure would make more pragmatic sense (i.e. what is empty error?).
  • Note that any instance of Alternative that tries to accumulate failures is likely to have a problem satisfying the distribution laws (5,6), as the rhs combines 4 potential failures and lhs combines 3.
    Would you expect (5,6) to hold in the context of a failure (e.g. parser error messages)? My answer is: I do not! Violating these laws is not necessarily a bad thing. The end result is that the programmer needs to make an explicit choice between p1 and p2 selecting one with the more desirable error output. The trade-off is similar to one made by the monad_validate package linked at the end of this article.

Let me return to the basic laws, particularly (2): u <|> empty = u:

import qualified Data.Attoparsec.ByteString as A 

testSuccess :: A.Parser a -> A.Result a
testSuccess p = A.parse p "foo"

testFail :: A.Parser a -> A.Result a
testFail p = A.parse p "bar"

u = A.string "foo"

lhs = u <|> empty
rhs = u

Here are the results:

-- >>> testFail lhs
-- Fail "bar" [] "Failed reading: empty"

-- >>>  testFail rhs
-- Fail "bar" [] "string"

So we broke the required second law. Incidentally, we would not be able to break this law using testSuccess.
This should not be surprising, (1-3) imply a monoidal structure on failures and this is not what attoparsec does.

attoparsec gets a lot of blame for its error output. Let’s try the IO alternative:

uIO = fail "foo" :: IO a

lhsIO = uIO <|> empty
rhsIO = uIO

ghci:

>>> lhsIO
*** Exception: user error (mzero)
>>> rhsIO
*** Exception: user error (foo)

We see the same issue.

One way to look at this, and I believe this is how some programmers are looking at this issue, is that any failure with any error message is considered equivalent to empty. The laws hold if the error information is ignored. Somewhat of a downer if you care about errors.

Side-Note: Numerous instances of Alternative manage to satisfy (2).
That includes ExceptT, the Validatation type listed at the end of this post. Here is the law working for ‘trifecta’

import Text.Trifecta
u = string "foo"

lhs = parseTest (u <|> empty) "bar" :: IO ()
rhs = parseTest u "bar" :: IO ()

ghci:

>>> lhs
(interactive):1:1: error: expected: "foo"
1 | bar<EOF> 
  | ^        
>>> rhs
(interactive):1:1: error: expected: "foo"
1 | bar<EOF> 
  | ^    

We will see the blueprint Monoid fix for (2) in the Either [e] a section.

Real-World Alternative (Optimism with Experience)

Here are some examples of problems arising from the use of Alternative semantics

Failure at the end

Laws are important, functional programmers use laws (sometimes even subconsciously) when thinking about, implementing, or designing the code. The second law tells us that we can slap a computation that always errors out at the end without messing things up.

Consider this (a slightly adjusted real-world) situation: your app needs to talk to an external website which can decide to do A, B, or C and will reply with A, B, or C json message. Based on what happened, your app will need to do different things. You need to parse the reply to know how to proceed.
The good news is that only A and B are needed in the short term, C can wait. For now, you are only required to tell the user when C happens.

This should be aeson but I keep attoparsec for consistency (the behavior is the same):

parseReply = parseA <|> parseB <|> parseC 
  where
    parseA = ...
    parseB = ...
    parseC = fail "C is not supported yet!"

The external website changed how they report A, now when A is processed parseA fails, the user sees: “C is not supported yet!”.

The following would be a slightly better code, the user would see parseB error message instead:

parseReply = parseC <|> parseA <|> parseB 

(sigh)

One way out is to parse A, B, and C separately and handle the results (and the parsing errors) outside of the Parser.

The other design risk is in thinking about the second law as ‘stable’: We will not disturb the computation too much if we append (add at the end of the <|> chain) a very restrictive parser that fails most of the time.
An example would be fixing an existing parser p with a missed corner case parser, p <|> cornerCaseP. Errors from p are now almost not visible.

So would cornerCaseP <|> p be a better solution? Next section covers that case.

Permissive computation at the end

This is the example I started with. Consider code like this:

specificComputation <|> bestEffortComputation

The specs may change and you will never learn that specificComputation no longer works because bestEffortComputation effectively hides the issue.

The way out is to run specificComputation and bestEffortComputation separately and handle results (e.g. parsing errors if the computation is a parser) outside, or come up with a different way of not using the alternative.

Failure at the end situation improves a bit with certain (other than aeson or attoparsec) alternatives, Permissive computation at the end does not seem to have a good available solution.

Pessimistic Instances

It would be ideal if the following property was true:

If a typeclass A is defined in the base package and A has something to do with failures, then there exist at least one instance of A in the base allowing to recover the error information

MonadFail fails this property (especially when combined with MonadPlus: Maybe Overuse - MonadFail).

Alternative fails it as well. (EDIT Feb 13, 2021: It has been pointed out to me on reddit by u/gcross that this is not a fair criticism. base typeclass definition does not really claim any relationship to failures. I stand corrected on this. I still think that it would be very nice to have error information friendly instance of Alternative in base.)

Can we come up with Alternative instances that do a decent job of maintaining error information? It seems that the answer is yes.

Either [e] a

This is a warm-up.

This instance is not new. It matches the ExceptT alternative instance from transformers / mtl. It uses standard Either monad and this is a MonadPlus (with a somewhat questionable right-zero law):

instance Monoid e => Alternative (Either e) where 
    empty  = Left mempty
    Left e1 <|> Left e2 = Left $ e1 <> e2
    (Left _) <|> r = r
    r <|> _ = r

(Note: transformers package has a conflicting instance in the deprecated Control.Monad.Trans.Error module, in a real code a newtype would be needed to avoid this conflict)

I have included Monoid e => Alternative (Either e) instance as a warm-up and to discuss the laws.

Laws:
The required (1-3) laws are satisfied without resorting to any sort of questionable reasoning that treats all errors as empty. However, empty represents a noOp failure computation (somewhat questionable meaning).

Optional (4 Right Zero) law (f <*> empty = empty) is questionable (consider f = Left e with a non-trivial e).
(7 Left Catch) is OK.
As we have predicted, the distribution laws are not satisfied.
(5) is NOT satisfied:

(f <|> g) <*> a = (f <*> a) <|> (g <*> a) 

If f and g represent successful computation and a is a list of errors then the rhs has twice as many errors as the lhs.

(6) is not satisfied either:

f <*> (b <|> c) = (f <*> b) <|> (f <*> c) 

If f represents a failed computation then rhs will duplicate f errors.
This looks like a bigger problem than it really is. The lhs and rhs contain the same amount of error information.

So, overall, Either [e] a has done quite well as an alternative!

A Decent Blueprint: Either [e] ([e], a)

What would really be nice, is to have a standard “right-catch with warnings” Alternative instance (please let me know if you have seen it somewhere on Hackage):

newtype ErrWarn e w a = EW {runEW :: Either e (w, a)} deriving (Eq, Show, Functor)
  
instance (Monoid e) => Alternative (ErrWarn e e) where 
    empty  = EW $ Left mempty
    EW (Left e1) <|> EW (Left e2) = EW (Left $ e1 <> e2)
    EW (Left e1) <|> EW (Right (w2, r)) = EW $ Right (e1 <> w2, r) -- coupling between @Either e@ and @(e,)@
    r@(EW (Right _)) <|> _ = r

This approach, when computing a <|> b, does not try to compute b if a succeeds. Thus, this instance matches the common left bias semantics. The approach accumulates all errors encountered up to the point of the first success and returns them as warnings.
This is a lawful Alternative (satisfies required laws (1-3)) and it does not rely on any questionable unification of empty with non-trivial errors.

I now feel justified using Monoid e constraint. Empty failure makes no sense, but empty warnings make a lot of sense!

But wait! To have Alternative we need Applicative. It is possible to implement Applicative for this type in more than one way, one even leads to a valid Monad and MonadPlus (with the right-zero caveat discussed above).
That approach is equivalent to WriterT w (Except e), it does not try to <*>-accumulate e-s, it only accumulates w-s:

instance (Monoid w) => Applicative (ErrWarn e w) where
    pure x = EW $ Right (mempty, x)
    EW (Left e) <*> _ = EW $ Left e
    EW (Right (u, f)) <*> EW (Right (v, x)) = EW (Right (u <> v, f x))
    EW (Right (u, f)) <*> EW (Left e)  = EW $ Left e

instance (Monoid w) => Monad (ErrWarn e w) where 
    EW (Left e) >>= _  = EW $ Left e
    EW (Right (u, x)) >>= k = 
        case k x of 
            EW (Right (v, b)) -> EW (Right (u <> v, b))
            EW (Left e) -> EW (Left e)

instance (Monoid e) => MonadPlus (ErrWarn e e)    

Please note the small difference. Standard transformers / mtl ExeptT and WriterT both support Alternative

 (Monad m, Monoid e) => Alternative (ExceptT e m)	
 (Monoid w, Alternative m) => Alternative (WriterT w m) -- Monoid only used to define @empty@

but in a decoupled way, ErrWarn couples these two by “writing” e-s.

This instance exhibits similar problems with matching the <*> semantics as the Monoid e => Either e instance from the previous section (i.e. (5,6) are not satisfied). Overall it is a very well behaved alternative.

Code Example

Here is a very convoluted (and not very good) parsing code that is intended only to demonstrate how ErrWarn works. This code creates a natural transformation from the attoparsec parser to ErrWarn and compares the error outputs from both.

This code will parse ByteStrings like “id last-first-name dept boss2” to produce, if successful, a hard-coded id, name, department, and boss name:

idP = 123 `onKeyword` "id"
nameP1 = "Smith John"  `onKeyword` "last-first-name"
nameP2 = fail "first-last-name not implemented yet"
deptP =  "Billing" `onKeyword` "dept"
bossP1 = "Jim K" `onKeyword` "boss1"     
bossP2 = "Kim J" `onKeyword` "boss2"    
bossP3 = pure "Mij K bosses everyone" 

onKeyword :: a -> B.ByteString -> AT.Parser B.ByteString a
onKeyword val key = const val <$> A.manyTill ACh.anyChar
                    (A.lookAhead $ A.string key)
                    A.<?> show key

data Employee = Employee {
    id :: Int
    , name :: String
    , dept  :: String
    , boss :: String
   } deriving Show

emplP :: A.Parser B.ByteString Employee
emplP = 
   Employee 
   <$> idP
   <*> (nameP1 <|> nameP2)
   <*> deptP
   <*> (bossP1 <|> bossP2 <|> bossP3) 

emplP' :: B.ByteString -> ErrWarn [String] [String] Employee
emplP' txt = 
   Employee 
   <$> ew idP 
   <*> (ew nameP1  <|> ew nameP2 )
   <*> ew deptP 
   <*> (ew bossP1  <|> ew bossP2 <|> ew bossP3)
   where
        ew :: A.Parser a  -> ErrWarn [String] [String] a
        ew p = singleErr $ A.parseOnly p txt

        singleErr :: Either e a -> ErrWarn [e] [e] a
        singleErr (Left e) = EW $ Left [e]
        singleErr (Right r) = EW $ Right ([], r)

Trying it with a good input:

-- >>> A.parseOnly emplP "id last-first-name dept boss1"
-- Right (Employee {id = 123, name = "Smith John", dept = "Billing", boss = "Jim K"})

-- >>> emplP' "id last-first-name dept boss1"
-- EW {runEW = Right ([],Employee {id = 123, name = "Smith John", dept = "Billing", boss = "Jim K"})}

Trying failure at the end situation (typo in "last-first-name"):

-- >>> A.parseOnly emplP "id last-firs-name dept boss2"
-- Left "Failed reading: first-last-name not implemented yet"

-- >>> runEW $ emplP' "id last-firs-name dept boss2"
-- Left ["\"last-first-name\": not enough input","Failed reading: first-last-name not implemented yet"]

(A similar benefit can be achieved by using one of the Hackage validation packages listed at the end of this post or using the ExceptT alternative.)

Trying permissive computation at the end situation ("boss" parsing error):

-- >>> A.parseOnly emplP "id last-first-name dept boss"
-- Right (Employee {id = 123, name = "Smith John", dept = "Billing", boss = "Mij K bosses everyone"})

-- >>>runEW $ emplP' "id last-first-name dept boss"
-- Right (["\"boss1\": not enough input","\"boss2\": not enough input"],Employee {id = 123, name = "Smith John", dept = "Billing", boss = "Mij K bosses everyone"})

(A similar benefit cannot be achieved by using a validation package from the list at the end of this post or a transformers stack. Please let me know if something like this exists elsewhere.)

We are no longer being thrown for a loop!

Extending Either [e] ([e], a)

The right-catch with warnings semantics of Either [e] ([e], a) is a decent principled computation that can be extended to other types. For example, a similar semantics could find its way into some parser internals.

I have created several prototype alternative instances (including a primitive WarnParser parser and ErrWarnT transformer) that follow the same semantics, they can be found in the linked repo.

ErrWarnT allows to program in a ErrWarnT e e f alternative (e.g. ErrWarnT e e Parser) and annotate additional error information on f (e.g. during parsing). This allows, for example, to pattern match on errors to figure out which alternatives in <|> have failed even if the overall computation has succeeded.
WarnParser accumulates <|> similar errors and warnings out of the box.

Rethinking the Typeclass Itself

Is Alternative a wrong abstraction for alternating failing computations? I think it is. IMO any abstraction used for working with failures should include failures in its semantics. Alternative typeclass does not do that.

Alternative is widely used and creating an ‘alternative’ to it will, probably, be very hard or even impossible. That typeclass would be useful only if the ecosystem accepted it.

One conceptually simple improvement would be to split Alternative to mimic the Semigoup / Monoid split (semigroupoids has Data.Functor.Alt which seems to fit the bill).
This would clean up some instances like ExceptT (the above Either [e]) or Validation by reducing the need for questionable empty definitions like Left []. Incidentally, this would be the opposite of the MonadZero proposal.

I would really like to see e-s in the typeclass definition:

class Applicative f => Semigroup1 f where
   (<|>)  :: f a -> f a -> f a 

class Applicative (f e) => Semigroup2 e f where
   (<||>)  :: f e a -> f e a -> f e a 

The linked repo contains some loose replacement ideas for Alternative and MonadPlus. It is a work in progress.

Alternative Beyond Parsing

It should be mentioned that there are instances of Alternative such as the list [], or ZipList where failures are not a concern. Sorting algorithms using MonadPlus are thumbs up. Examples like LogicT or other backtracking search mechanisms should be in the same boat (at least from the failure point of view, other aspects can be questionable and fascinating stackoverflow on mplus associativity).

Also, these instances are rather cool.
Languages like JavaScript, Python, Groovy have a concept of truthiness. Truthy Falsy are a thing and come with a Boolean algebra of sorts. Try evaluating this in you browser’s console:

> "hello" || ""
"hello"
> "" || "hello"
"hello"

Truthiness is questionable because the Boolean algebra laws (like a || b = b || a) no longer hold.

Now try these in ghci:

>>> "" <|> "hello"
"hello"
>>> "hello" <|> ""
"hello"

Alternative is a principled version of the truthiness. The laws properly state the algebra limitations.
As we have seen, the problem is in going with this generalization too far.

async package uses <|> to return result form the computation that finishes first. This seems a good use to me.

Several types like ExceptT, Validation (see hackage section below) allow to use user defined monoid error types. mempty may not have much sense as an error, but this setup offers interesting options for accumulating errors. For example, using it with Data.Monoid.Max could be very interesting.

Not so good:
An interesting case is the STM monad. a <|> b is used to chain computations that may want to retry. I imagine, composing STM computations this way is rare. If you wanted to communicate why a has decided to retry, how would you do that? I consider STM use of alternatives problematic.

IO itself is an Alternative and uses <|> as a catch that throws away the error information. I dislike the IO instance. “Launching missiles” and not knowing what went wrong seems not ideal.

Relevant work on Hackage

free package contains a semantic (free) version of Alternative.

semigroupoids offers Alt that is just a Functor and does not need to have empty.

transformers ExceptT implements Alternative which accumulates “Lefts”.

A list of interesting packages that implement Monoid-like semantics for Applicative (most also implement Alternative) to accumulate errors provided by u/affinehyperplane on reddit:

either defines Either like Validation e a applicative, both <|> and <*> accumulate errors
validation defines a similar Validation type, it does not define alternative instance.
validation-selective defines a similar Validation type loaded with (non-monad) instances
monad-validate provides an interesting and very useful validation monad transformer (this is lawful if you do not compare error outputs) that can accumulate errors, it does not implement Alternative.

In the context of parsers, it should be noted that packages like trifecta, (mega)parsec do nice job returning error messages when <|> fails.

A good references about Alternative and MonadPlus in general is the Typeclassopedia and wikibooks, both contain interesting links.

There are many stackoverflow answers about Haskell solutions to accumulating errors. These typically refer to some of the packages in the above list, I am not linking them here.
There are many, many discussions about error output from different parsing libraries. These are typically focused on criticizing a particular package (typically attoparsec) not the Alternative typeclass itself.
I am sure, this list is not complete. Please let me know if you see a relevant work elsewhere.

Conclusions, Thoughts

It is possible to implement instances that do a decent error management but it feels like this is accomplished despite of the Alternative typeclass definition and its laws. To answer my title: IMO Alternative is a wrong abstraction for managing computational failures.

The more I program in Haskell the more I view Functional Programming as a branch of Applied Mathematics.
Criticizing mathematical abstractions does not make much sense. Criticism of how well an abstraction fits is application if a fair game.

Why errors are being overlooked? I assembled a possible list when writing about the Maybe Overuse and that list seems to translate well to the alternative typeclass. For example, code using <|> is very terse, something with a stronger error semantics will most likely be more verbose; coding with <|> is simple, stronger error semantics will likely be more complex …
I could be wrong on this: the original usages of MonadPlus were probably related to sorting/searching and lists. Alternative computations with more complex error structure were probably introduced later? … and, the instances ended up outgrowing the typeclass?

The pessimist theme was partially inspired by the following two concepts:
Positivity Bias and, its opposite, the Negativity Bias are psychological notions that, I believe, have a deep relevance to the programming in general.
Positivity Bias includes a tendency to favor positive information in reasoning and, by definition, will make you think about “happy path” and “sunny day scenarios”.
Negativity Bias includes a tendency to favor negative information in reasoning and, by definition, will make you consider “rainy day scenarios”, corner cases, error handling, error information.
I think we should embrace some form of pessimism and put in on the pedestal next to the principled construction.

I hope this post will motivate more discussion about the error information handling in Haskell.
My particular interest is in discussing:

  • your views about rethinking the Alternative typeclass
  • your views on pessimism in programming
  • your views on the error information loss in Haskell code
  • is ErrWarn somewhere on Hackage and I did not see it?
  • other interesting Alternative instances that care about errors
  • obviously, anything that I got wrong

reddit discussion
github discussions

Thank you for reading!

Reader’s Response

Common critical response on reddit (2021-02-15) can be summarized as: Alternative should not be used like this and there are better ways of writing such code.

“it seems a bit odd, to me, to criticize and talk about rethinking a typeclass because it does something that it was never intended to do”
“people should be using a typeclass designed for handling errors”
“maybe you are asking too much to Alternative”

The ErrWarn blueprint or other instances with strong error information preserving abilities were not discussed.

There appears to be mixed response to the error information loss being a problem in general. Some readers claim that this is not a problem, some seem to share my concern.

Author’s Defense

I agree with the: “Alternative should not be used like this”.

Alternative is an example of an abstraction that is very easy to use, it makes coding fast.
It most likely will end up being used (I have seen it) in ways similar to what I described in this post.
It is important that the developers are aware of the gotchas that come with some of the instances.

Any code (alternative or not) producing confusing error output is a concern.
IMO, every abstraction and every instance needs to be concerned about the error output quality. Not being designed for error handling should not be a thing. At the same time, error friendlier instances should be a good thing.