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