sandwood
sandwood

Reputation: 2167

Mixing Either and Maybe Monads

I think I understand how to cascade Monad of the same type. I would like to combine two Monads together to perform an operation based on them :

I think the code below resume the problem : suppose we have a function that validates that a String contains "Jo" and append "Bob" to it if it's the case, and another one that validates that the String length is > 8

The hello function would apply the first , then the second on the result of the first and return "Hello" to all that in case of success or 'Nothing' (I don't know what is this 'Nothing' btw , Left or Nothing) in case of error.

I believe that it's around Monad transformer what I need but I could not find a concise example that would help me to start.

I precise that this is nothing theoratical as there is around Haskell package that works with Either and others that works with Maybe

validateContainsJoAndAppendBob :: String -> Maybe String
validateContainsJoAndAppendBob l =
  case isInfixOf "Jo" l of
    False -> Nothing
    True  -> Just $ l ++ "Bob"

validateLengthFunction :: Foldable t => t a -> Either String (t a)
validateLengthFunction l =
  case (length l > 8)  of
    False -> Left "to short"
    True  -> Right l

-- hello l = do
  -- v <- validateContainsJoAndAppendBob l
  -- r <- validateLengthFunction v
  -- return $ "Hello " ++ r 

Upvotes: 4

Views: 2118

Answers (3)

chepner
chepner

Reputation: 532303

What you want is (in the categorical sense) a natural transformation from Maybe to Either String, which the maybe function can provide.

maybeToEither :: e -> Maybe a -> Either e a
maybeToEither e = maybe (Left e) Right


hello l = do
  v <- maybeToEither "No Jo" (validateContainsJoAndAppendBob l)
  r <- validateLengthFunction v
  return $ "Hello " ++ r

You can use <=< from Control.Monad to compose the two validators.

hello l = do
    r <- validateLengthFunction <=< maybeToEither "No Jo" . validateContainsJoAndAppendBob $ l
    return $ "Hello " ++ r

You can also use >=> and return to turn the whole thing into a single monstrous point-free definition.

hello =      maybeToEither "No Jo" . validateContainsJoAndAppendBob
         >=> validateLengthFunction
         >=> return . ("Hello " ++)

Upvotes: 5

Mark Seemann
Mark Seemann

Reputation: 233407

In addition to the practical answer given by Li-yao Xia, there's other alternatives. Here's two.

Maybe-Either isomorphism

Maybe a is isomorphic to Either () a, which means that there's a lossless translation between the two:

eitherFromMaybe :: Maybe a -> Either () a
eitherFromMaybe (Just x) = Right x
eitherFromMaybe  Nothing = Left ()

maybeFromEither :: Either () a -> Maybe a
maybeFromEither (Right x) = Just x
maybeFromEither (Left ()) = Nothing

You can use one of these to translate to the other. Since validateLengthFunction returns an error text on failure, it would be a lossy translation to turn its return value into a Maybe String value, so it's better to use eitherFromMaybe.

The problem with that, though, is that this will only give you an Either () String value, and you need an Either String String. You can solve this by taking advantage of Either being a Bifunctor instance. First,

import Data.Bifunctor

and then you can write hello as:

hello :: String -> Either String String
hello l = do
  v <-
    first (const "Doesn't contain 'Jo'.") $
    eitherFromMaybe $
    validateContainsJoAndAppendBob l
  r <- validateLengthFunction v
  return $ "Hello " ++ r 

This essentially does the same as Li-yao Xia's answer - a little less practical, but also a little less ad-hoc.

The first function maps the first (left-most) case of an Either value. In this case, if the return value from validateContainsJoAndAppendBob is a Left value, it's always going to be Left (), so you can use const to ignore the () input and return a String value.

This gets the job done:

*Q49816908> hello "Job, "
Left "to short"
*Q49816908> hello "Cool job, "
Left "Doesn't contain 'Jo'."
*Q49816908> hello "Cool Job, "
Right "Hello Cool Job, Bob"

This alternative I prefer to the next one, but just for completeness' sake:

Monad transformers

Another option is using Monad transformers. You can either wrap the Maybe in an EitherT, or conversely wrap an Either in MaybeT. The following example does the latter.

import Control.Monad.Trans (lift)
import Control.Monad.Trans.Maybe (MaybeT(..))

helloT :: String -> MaybeT (Either String) String
helloT l = do
  v <- MaybeT $ return $ validateContainsJoAndAppendBob l
  r <- lift $ validateLengthFunction v
  return $ "Hello " ++ r

This also works, but here you still have to deal with the various combinations of Just, Nothing, Left, and Right:

*Q49816908> helloT "Job, "
MaybeT (Left "to short")
*Q49816908> helloT "Cool job, "
MaybeT (Right Nothing)
*Q49816908> helloT "Cool Job, "
MaybeT (Right (Just "Hello Cool Job, Bob"))

If you want to peel off the MaybeT wrapper, you can use runMaybeT:

*Q49816908> runMaybeT $ helloT "Cool Job, "
Right (Just "Hello Cool Job, Bob")

In most cases, I'd probably go with the first option...

Upvotes: 7

Li-yao Xia
Li-yao Xia

Reputation: 33569

Use a function to convert Maybe to Either

note :: Maybe a -> e -> Either e a
note Nothing e = Left e
note (Just a) _ = Right a

hello l = do
  v <- validateContainsJoAndAppendBob l `note` "Does not contain \"Jo\""
  r <- validateLengthFunction v
  return $ "Hello " ++ r

Upvotes: 8

Related Questions