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.
CodePudding user response:
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
CodePudding user response:
You're running into a design limitation of monad transformer classes as implemented by the mtl
package. MonadReader
, MonadState
, etc., all have this same functional dependency, even though it might be convenient to ask
for different contexts, or get
and put
to different states in the same monad.
I believe that mtl
could be modified to allow for type-dependent dispatch to similar layers, though it would involve more than just removing the functional dependencies. Specifically, overlapping instances for the MonadXxx
classes would need to be added to select between handling an action in the current layer (if the types match) or lifting to the next layer (if they don't). For many layers, this change would come with a significant usability cost. Consider:
count :: MonadState Int m => m ()
count = modify ( 1)
This type checks only because the functional dependency ensures that the modify
"knows" it's modifying an Int
. Without the functional dependency, modify ( 1)
is just modifying some Num a => a
state. It might be an Int
, or it might be some Double
in a different layer.
The problem is less severe for throwError
, since you'll typically be throwing a "known" type, so throwError err1
isn't ambiguous. Functions like tryError
and patterns that catch and rethrow errors would need to be annotated though, even for the majority of users who are using only one error layer, where the error being so handled should be unambiguous.
Effect systems as proposed by Oleg Kiselyov et al. (e.g., extensible-effects
or freer-simple
) more explicitly reflect effects at the type level, and can differentiate between two reader layers that read different context types, or two exception layers that handle different exception types. So, you might want to look into those. For example, using freer-simple
, your program would look like:
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE MonoLocalBinds #-}
import Control.Monad.Freer
import Control.Monad.Freer.Error
data Err1 = Err1
data Err2 = Err2
f :: (Member (Error Err1) effs) => Eff effs Int
f = throwError Err1
g :: (Member (Error Err2) effs) => Eff effs Int
g = throwError Err2
h :: (Members '[Error Err1, Error Err2] effs) => Eff effs Int
h = throwError Err2
main = do
-- this pipeline runs the monadic action `h` to produce an:
-- Either Err1 (Either Err2 Int)
case run . runError . runError $ h of
Left Err1 -> error "err1"
Right (Left Err2) -> error "err2"
Right (Right i) -> putStrLn $ "result: " show i
These systems pay the usability cost mentioned above. The following needs a type application or similar to type check:
count :: (Member (State Int) effs) => Eff effs ()
count = modify @Int ( 1)