https://www.parsonsmatt.org/2018/11/03/trouble_with_typed_errors.html

You, like me, program in either Haskell, or Scala, or F#, or Elm, or PureScript, and you don’t like runtime errors. They’re awful and nasty! You have to debug them, and they’re not represented in the types. Instead, we like to use Either (or something isomorphic) to represent stuff that might fail:

data Either l r = Left l | Right r

Either has a Monad instance, so you can short-circuit an Either l r computation with an l value, or bind it to a function on the r value.

So, we take our unsafe, runtime failure functions:

head   :: [a] -> a
lookup :: k -> Map k v -> v
parse  :: String -> Integer

and we use informative error types to represent their possible failures:

data HeadError = ListWasEmpty

head :: [a] -> Either HeadError a

data LookupError = KeyWasNotPresent

lookup :: k -> Map k v -> Either LookupError v

data ParseError
    = UnexpectedChar Char String
    | RanOutOfInput

parse :: String -> Either ParseError Integer

Except, we don’t really use types like HeadError or LookupError. There’s only one way that head or lookup could fail. So we just use Maybe instead. Maybe a is just like using Either () a – there’s only one possible Left () value, and there’s only one possible Nothing value. (If you’re unconvinced, write newtype Maybe a = Maybe (Either () a), derive all the relevant instances, and try and detect a difference between this Maybe and the stock one).

But, Maybe isn’t great – we’ve lost information! Suppose we have some computation:

foo :: String -> Maybe Integer
foo str = do
    c <- head str
    r <- lookup str strMap
    eitherToMaybe (parse (c : r))

Now, we try it on some input, and it gives us Nothing back. Which step failed? We actually can’t know that! All we can know is that something failed.

So, let’s try using Either to get more information on what failed. Can we just write this?

foo :: String -> Either ??? Integer
foo str = do
    c <- head str
    r <- lookup str strMap
    parse (c : r)

Unfortunately, this gives us a type error. We can see why by looking at the type of >>=:

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

The type variable m must be an instance of Monad, and the type m must be exactly the same for the value on the left and the function on the right. Either LookupError and Either ParseError are not the same type, and so this does not type check.

Instead, we need some way of accumulating these possible errors. We’ll introduce a utility function mapLeft that helps us:

mapLeft :: (l -> l') -> Either l r -> Either l' r
mapLeft f (Left l) = Left (f l)
mapLeft _ r = r