MonadThrow vs MonadError - Haskell

Welcome to the Functional Programming Zulip Chat Archive. You can join the chat here.

Sridhar Ratnakumar

What do you think of designing API functions to use MonadError to handle errors instead of MonadThrow? I think the former is better because you specify the exact type of the error the API will return.

Sridhar Ratnakumar

The path library doesn't do this, so I've initiated a discussion there: https://github.com/commercialhaskell/path/issues/149

The problem with using MonadThrow is that the caller can theoretically expect just about any exceptions to be thrown. Whereas with MonadError e, the now-small range of errors that the caller can ex...
Lukwago Allan

I tend to prefer MonadError too, it makes error code/handling more explicit

Alexis King

IMO, they serve different purposes—MonadThrow and MonadCatch are really about dealing with IO exceptions (though they have some other instances), and I mostly avoid MonadThrow because I usually don’t want to use IO exceptions. I’d much rather deal with an API that provides MonadError constraints than one that provides MonadThrow constraints.

That said, it’s worth noting that MonadMask is a different beast: it’s fundamentally something MonadError cannot provide, because MonadMask allows you to implement a finally operation that runs if any monad in the stack causes an abort. For example, if you have ExceptT e IO, Control.Monad.Catch.finally will execute the action whether you raise an IO exception or call throwError. In that sense, MonadMask is “deep” while MonadError is “shallow.” You need MonadMask if you need reliable recovery from aborts; MonadError just won’t cut it.

Sridhar Ratnakumar

Why does MonadError e m have the functional dependency m -> e?

TheMatten

@Sridhar Ratnakumar From instances it seems like underlying m always determines e

Sridhar Ratnakumar

Well I'm looking at the Either instance - and I don't see Either e determining e ... oh wait!

Sridhar Ratnakumar

Okay this does surprise me at first glance: pasted image

Sridhar Ratnakumar

It is surprising, because here I'm thinking "why doesn't compiler use one instance for the first let statement, and another for the other let statement"

Daniel Díaz Carrete

Looks like the type annotation should be in the _s, not on the left side.

Sridhar Ratnakumar

Even this fails:

main :: IO ()
main = do
  () <- maybe (fail "Invalid") pure $ someFunc @Maybe
  _v <- either (fail . show) pure $ someFunc @(Either MyError)
  pure ()```
TheMatten

@Sridhar Ratnakumar https://www.stackage.org/haddock/lts-14.15/mtl-2.2.2/Control-Monad-Error.html#t:MonadError - there's only one instance for Maybe (with ()) - but someFunc wants one that uses MyError

TheMatten

(There can only be one because of fundep)

Sridhar Ratnakumar

Yea, the question is why is the second invocation not using the Either instance?

Sridhar Ratnakumar

Why does the compiler not use two instances in the same function (main)?

Sridhar Ratnakumar

I mean, I explicitly specified Either MyError as the monad in the second invocation; so GHC should have no trouble, in theory, figuring out the right instance to use?

TheMatten

Error talks about first invocation

Sridhar Ratnakumar

Uh, right. I didn't see. Maybe was a bad choice.

Sridhar Ratnakumar

I'm trying to understand the problem Chris points to in the 3rd paragraph of https://github.com/commercialhaskell/path/issues/149#issuecomment-558069993

The problem with using MonadThrow is that the caller can theoretically expect just about any exceptions to be thrown. Whereas with MonadError e, the now-small range of errors that the caller can ex...
Sridhar Ratnakumar

So if I use a custom error type (MyError) with MonadError - the only instance I use for the m is Either e?

Sridhar Ratnakumar

(plus, transformer versions)

TheMatten

You couldn't use it with IO, because IO is bound to IOException (even though it could possibly throw any), while Maybe wouldn't work because it's bound to () (even though you could just ignore error's content and return Nothing)

Sridhar Ratnakumar

This works:

main :: IO ()
main = do
  () <- either throwM pure someFunc
  let _ :: Either MyError () = someFunc
  pure ()

IO and Either e at the same time.

TheMatten

Actually, now that I think about it - catchError cannot be implemented properly for polymorphic Maybe instance, because you lose original error

TheMatten

You could Monoid yourself out though if it's content is not important

TheMatten

You can have safe MonadThrow e Maybe though because catch is in MonadCatch

TheMatten

Think of it this way - MonadError sort of requires m to "keep" original error in case it occurs (catchError is "proof" of that) - this can't be satisfied by Maybe properly
But MonadThrow separates that proof to MonadCatch - so even though Maybe can't have MonadCatch instance, it can have MonadThrow

Sridhar Ratnakumar

Yes, but with MonadThrow|Catch we are dealing with bottoms which "bypass" a strict type system.