Question

I know it's possible to change the wrapped type, so that you can have

f :: (a -> m b)
g :: (b -> m c)
f >>= g :: (a -> m c)

but is it possible to change m? If m is a MonadError and is implemented both by an Either ErrorA and Either ErrorB, can i somehow chain them? Obviously I can't chain them directly, because what would be the type of Left? However, I'm in a situation where I end up calling show in either case, but I haven't found a better solution than

case mightFail1 of
  Left e -> show e
  Right v -> either show doStuff mightFail2

which fails to properly use the monadic behavior of stopping at the first error without me having to check explicitly.

Was it helpful?

Solution 2

It is not possible to do this in a monadic chain.

Note that this isn't really concerned with Monad at all: you're not in any way binding a nested monadic action in the Left argument or something like that, but you're only transforming the argument itself. It's basically the functor operation fmap, but on the Left instead of Right part:

fmap     :: (r->ρ) -> Either l r -> Either l ρ
fmapLeft :: (l->λ) -> Either l r -> Either λ r

A function with that particular signature, surprisingly, doesn't seem to exist. However, this idea of a functor with two covariant arguments is obviously more general than just Either, and indeed there's a dedicated class. It has (IMO rather unfortunate naming, clashes with Arrow)

Data.Bifunctor.first :: (a -> b) -> p a c -> p b c

which specialises in fact to

first :: (a -> b) -> Either a c -> Either b c

So you can use

f :: (Show a) => (Either a b) -> (Either String b)
f = first show

OTHER TIPS

The entire notion of "changing the container" is called a "natural transformation". Specifically, we want a function which transforms containers without affecting what's inside. We can ensure this is the case in the type system by using forall.

-- natural transformation
type (m :~> n) = forall a. m a -> n a

Then these can be applied whenever we want. For instance, if you can transform ErrorA -> ErrorB then there's a general operation for you

mapE :: (e -> e') -> (Either e :~> Either e')
mapE f (Left e)  = Left (f e)
mapE _ (Right a) = a

You can even get really fancy with type operators and sum types.

-- a generic sum type
infixr 9 :+:
newtype (a :+: b) = Inl a | Inr b

liftE :: (Either e :~> Either (e' :+: e))
liftE = mapE Inr

Bifunctors achieve roughly the same effect but they are a totally different way of viewing the problem. Instead of generally changing out the container, they affect another covariant parameter (or index) in the container itself. So, Bifunctor actions can always be seen as natural transformations, but NTs are more general.

For this specific case you can use fmapL from my errors library:

fmapL :: (a -> b) -> Either a r -> Either b r

Since you said that you were going to show both of them eventually, you can use fmapL show to unify both of them to agree on the Either String monad and sequence them directly:

do v <- fmapL show mightFail1
   fmapL show $ mightFail2 v

Now you can sequence both of them using do notation and have them share the same error handling mechanism.

Note that show is not the only way to unify the left values. You can also unify non-showable values using ... Either!

example :: Either (Either Error1 Error2) ()
example = do
    v <- fmapL Left mightFail1
    fmapL Right $ mightFail2 v

StackOverflow is an excellent rubber duck. I found a solution, but I'm still curious if there is another approach. Since I already finished writing the question, I'll post it anyway.

Instead of thinking of "changing the monad type during the chain", simply transform all values before they are chained, by making them return Either String a:

f :: (Show a) => (Either a b) -> (Either String b)
f = either (Left . show) (Right)

This wraps the whole call to either (f mightFail1), there might be a composable variant (f . mightFail1)

Crazy mode: wrap either into a newtype, make it an instance of functor that maps the function on the left side instead of the right side, and just call fmap show mightFail1 (don't forget to wrap and unwrap your newtype). does that make sense? :D

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top