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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With