Last Modified on February 17, 2021
- (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
- (2022.08.30) reclassified under patterns-of-erroneous-code tag
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
andmany
run the computation until first failure and return the successful results,some
expects at least one success, otherwise it will fail.some
andmany
are a nod towards parsers or other computations that change state.some
andmany
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
andmany
provide no error information about the failure that ended the production of the result list. I will not discusssome
ormany
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:
<|> u = u -- (1)
empty <|> empty = u -- (2)
u <|> (v <|> w) = (u <|> v) <|> w -- (3) u
Note that these laws do not link Applicative
and Alternative
in any way. That happens in the following, optional, set of laws:
<*> empty = empty -- (4) Rigth Zero
f <|> b) <*> c = (a <*> c) <|> (b <*> c) -- (5) Left Distribution
(a <*> (b <|> c) = (a <*> b) <|> (a <*> c) -- (6) Right Distribution
a pure a) <|> x = pure a -- (7) Left catch (
For example, when writing a parser you may decide to use one of these approaches:
= Employee <$> employeeIdParser <*> (nameParser1 <|> nameParser2)
p1 = (Employee <$> employeeIdParser <*> nameParser1) <|> (Employee <$> employeeIdParser <*> nameParser2) p2
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 beotherFailure
notempty
.
- (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 isempty
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 betweenp1
andp2
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
= A.parse p "foo"
testSuccess p
testFail :: A.Parser a -> A.Result a
= A.parse p "bar"
testFail p
= A.string "foo"
u
= u <|> empty
lhs = u rhs
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:
= fail "foo" :: IO a
uIO
= uIO <|> empty
lhsIO = uIO rhsIO
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
= string "foo"
u
= parseTest (u <|> empty) "bar" :: IO ()
lhs = parseTest u "bar" :: IO () rhs
ghci:
>>> lhs
:1:1: error: expected: "foo"
(interactive)1 | bar<EOF>
| ^
>>> rhs
:1:1: error: expected: "foo"
(interactive)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):
= parseA <|> parseB <|> parseC
parseReply where
= ...
parseA = ...
parseB = fail "C is not supported yet!" parseC
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:
= parseC <|> parseA <|> parseB parseReply
(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:
<|> bestEffortComputation specificComputation
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
= Left mempty
empty 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:
<|> g) <*> a = (f <*> a) <|> (g <*> a) (f
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:
<*> (b <|> c) = (f <*> b) <|> (f <*> c) f
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
= EW $ Left mempty
empty 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,)@
@(EW (Right _)) <|> _ = r 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:
= 123 `onKeyword` "id"
idP = "Smith John" `onKeyword` "last-first-name"
nameP1 = fail "first-last-name not implemented yet"
nameP2 = "Billing" `onKeyword` "dept"
deptP = "Jim K" `onKeyword` "boss1"
bossP1 = "Kim J" `onKeyword` "boss2"
bossP2 = pure "Mij K bosses everyone"
bossP3
onKeyword :: a -> B.ByteString -> AT.Parser B.ByteString a
= const val <$> A.manyTill ACh.anyChar
onKeyword val key $ A.string key)
(A.lookAhead 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
= singleErr $ A.parseOnly p txt
ew p
singleErr :: Either e a -> ErrWarn [e] [e] a
Left e) = EW $ Left [e]
singleErr (Right r) = EW $ Right ([], r) singleErr (
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.