user1002430
user1002430

Reputation:

Is it possible to leverage monadic structure if multiple monads are "mixed"?

Consider the following code:

run = runExcept $ do
  case Just 1 of
    Nothing -> throwE "escape 1"
    Just x -> do
      case Just 2 of
        Nothing -> throwE "escape 2"
        Just y -> do
          case Just 3 of
            Nothing -> throwE "escape 3"
            Just z -> return z

Pretend the Just 1, Just 2, Just 3 are function calls that return Maybe Int.

This whole function is inside an ExceptT because I want to throw exceptions. But inside is really just a lot of Maybe values being manipulated.

So, is it possible for me to leverage the behaviour of a Maybe monad to avoid the stair-casing while still being able to throw exceptions?

Upvotes: 3

Views: 129

Answers (1)

Benjamin Hodgson
Benjamin Hodgson

Reputation: 44634

It seems that you want to enrich a sequence of Maybe actions with information pertaining to which one failed, if any. Why not implement enriching as a simple function?

enrich :: MonadError e m => e -> Maybe a -> m a
enrich e Nothing = throwError e
enrich e (Just x) = return x

I'm using MonadError - the class of monads m which can throw errors of type e - to generalise the type of enrich. For example, we can use it as if it had the type e -> Maybe a -> Except e a or e -> Maybe a -> Either e a, because Except and Either are both instances of MonadError.

Now you just need to use enrich to lift your Maybe values into the richer monadic context one at a time.

action = do
    x <- enrich "escape 1" maybe1  -- look mum, no staircasing!
    y <- enrich "escape 2" maybe2
    z <- enrich "escape 3" maybe3
    return [x, y, z]

If you're using your monad applicatively - that is, you're not using earlier results to determine later computations - there's an idiomatic way to generalise this function to work over an arbitrary number of Maybes. We're going to shove the Maybes into a list, along with the extra data we need to enrich it - in this case, the error message. Then we can traverse (née mapM) the list to enrich each Maybe inside it and join them up into a bigger monadic action.

enrichMaybes :: (Traversable t, MonadError e m) => t (e, Maybe a) -> m (t a)
enrichMaybes = traverse (uncurry enrich)

action = enrichMaybes [("escape 1", maybe1), ("escape 2", maybe2), ("escape 3", maybe3)]

The ability to treat effects as first-class citizens like this is why functional programming is a Good Thing.

Upvotes: 6

Related Questions