Home > Net >  Why does `MonadError` have a functional dependency?
Why does `MonadError` have a functional dependency?

Time:01-06

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)
  • Related