Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does `MonadError` have a functional dependency?

Tags:

haskell

monads

It seems to be that this may be useful:

data Err1 = Err1
data Err2 = Err2

f :: (MonadError Err1 m) => m Int
f = _

g :: (MonadError Err2 m) => m Char
g = _ 

h :: (MonadError Err1 m, MonadError Err2 m) => m (Int, Char)
h = (,) <$> f <*> g

And whilst the above compiles, h is unusable, because once one attempts to define two instances of MonadError for the same m, the functional dependencies of the MonadError class are going to cause an error.

The question I ask is why does MonadError have this functional dependency? It seems to reduce its flexibility, so I presume there is some benefit? Could one give an example of code using MonadError which would be worse/wouldn't work if the functional dependency was removed? Something makes me think I'm missing something about how MonadError is intended to be used as the functional dependency just seems to get in the way.

like image 944
Clinton Avatar asked Sep 17 '25 13:09

Clinton


1 Answers

Suppose that your h function worked: there is some monad which has a MonadError instance for both Err1 and Err2. Very well, throwError is a method of that class, and so throwError (e1 :: Err1) and throwError (e2 :: Err2) are both valid. Someone may now write this:

err1ToCode :: forall m. MonadError Err1 m => m Int
err1ToCode = foo `catchError` handle
  where handle :: Err1 -> m Int
        handle (Err1 errorCode) = pure errorCode

It seems quite reasonable to assume that err1ToCode never throws an exception: any exception that foo throws is converted to its error code instead. Guarantees like this make it more predictable to work with errors. But if foo may throw exceptions of some other, unknown type as well (e.g. Err2), then err1ToCode may throw an error despite apparently catching all errors.

So I don't think it's fundamentally impossible to give a type multiple MonadError instances, but it makes things less pleasant for the majority of cases: you can never assume you've caught and handled all errors, because some instance with yet another error type may be lurking out there.

And I don't see what particular benefit it brings, either. You want to be able to throw multiple types of errors, which is fair enough - lots of languages have exception facilities that allow this. But you can do this without needing multiple MonadError instances. Just use an error type that encompasses all the errors you want to throw:

data KnownErrors = E1 Err1 | E2 Err2

f :: MonadError KnownErrors m => m Int
f = pure 1
g :: MonadError KnownErrors m => m Char
g = throwError (E2 (Err2 "broken"))
h :: MonadError KnownErrors m => m (Int, Char)
h = (,) <$> f <*> g
like image 105
amalloy Avatar answered Sep 19 '25 06:09

amalloy