Composing Exceptions with MonadError

Jonathan Fischoff
3 min readMay 21, 2017

--

Always be hallwaying

I was discussing exception handling with Sukant Harja at the C◦mp◦se :: Conference and he pointed out a common frustration: how to precisely specify exceptions while still having composable functions.

For instance, say I have a function that can throw a “divide by zero” exception,

data ZeroDivisor = ZeroDivisorsafeDivsion :: MonadError ZeroDivisor m 
=> Double -> Double -> m Double
safeDivsion x y
| y == 0 = throwError ZeroDivisor
| otherwise = return $ x / y

and I have a function that can throw an “id not found” exception,

newtype IdNotFound = IdNotFound IntlookupId :: MonadError IdNotFound m => Int -> m String
lookupId theId
| theId == 1 = return "Foo"
| otherwise = throwError $ IdNotFound theId

it is not immediately clear how to combine these functions such that a straightforward transformer stack can be used.

If Haskell had extendible sum types, I think this would be easy to solve; alas, it does not (I think OCaml does…maybe…perhaps PureScript?).

The Blog Post Could End Here

Rúnar Bjarnason pointed out that this problem is largely solved if you are using an effects library like extendible-effects. I haven’t used extendible-effects, but from a cursory view of throwExc I agree with Rúnar.

Extendible Exceptions with mtl

I had forgotten to immediately mention it, because I had forgotten I had written it, but I had a possible solution formtl based on an extendible sum type.

I made an extendible sum type using a GADT similar to the one from generics-sop:

data Variant :: [*] -> * where
Z :: x -> Variant (x ': xs)
S :: Variant xs -> Variant (x ': xs)

I then created some Prism’s using techniques from vinyl:

data Nat = NZ | NS !Nattype family RIndex (r :: k) (rs :: [k]) :: Nat where
RIndex r (r ': rs) = NZ
RIndex r (s ': rs) = NS (RIndex r rs)
class i ~ RIndex x xs => Has xs x i where
inj :: Prism’ (Variant xs) x
instance Has (x ‘: xs) x NZ where
inj = prism’ Z $ \case
Z x -> Just x
_ -> Nothing
_S :: Prism' (Variant (x ': xs)) (Variant xs)
_S = prism' S $ \case
S v -> Just v
_ -> Nothing
instance (RIndex x (a ': xs) ~ NS i, Has xs x i)
=> Has (a ': xs) x (NS i) where
inj = _S . inj

I made a helper function to throw an exception:

throwE :: (Has xs e i, MonadError (Variant xs) m) => e -> m a
throwE = throwError . review inj

I can now write a function that makes it clear which exceptions it throws,

data ZeroDivisor = ZeroDivisorsafeDivsion :: (Has xs ZeroDivisor i, MonadError (Variant xs) m) 
=> Double -> Double -> m Double
safeDivsion x y
| y == 0 = throwE ZeroDivisor
| otherwise = return $ x / y

and another function that throws a different exception:

newtype IdNotFound = IdNotFound IntlookupId :: (Has xs IdNotFound i, MonadError (Variant xs) m) 
=> Int -> m String
lookupId theId
| theId == 1 = return "Foo"
| otherwise = throwE $ IdNotFound theId

Then I can combine the two functions and get a new function that makes it clear from the type signature that it can throw both.

combine :: ( Has xs IdNotFound i
, Has xs ZeroDivisor i'
, MonadError (Variant xs) m
)
=> m ()
combine = do
safeDivsion 1 0
lookupId 1
return ()

Finally, we can handle the exceptions by using our extendable sum type.

test :: IO ()
test = case runExcept combine of
Right () -> putStrLn "success"
Left v -> case v :: Variant [IdNotFound, ZeroDivisor] of
S (Z ZeroDivisor) -> putStrLn "zero divisor!"
Z (IdNotFound i) -> putStrLn
$ "Id " ++ show i ++ " not found!"

I haven’t used this. It feels too complicated for a problem I don’t find bothersome. I think if one is willing to give up pattern matching, there is probably a simpler way to implement this … without all the type level shenanigans that make Haskell a pain to use (protip: don’t break type inference).

It is a point in the design space. The code is on GitHub. Feel free to give it a whirl: https://github.com/jfischoff/prism-exception-experiments

--

--