Handling nested Maybe in Haskell

When I first started writing Haskell, there was a tendency that my codes will shift to the right that it becomes really hard to read. This is mostly due to the use of Maybe or Either, and it is often that I need to unwrap value from these constructs and make a decision based on the unwrapped value.

case mDash of
  Nothing   -> sendErrorResponseString "Can't find dashboard"
  Just dash -> do
    case dashboardPanels . entityVal $ dash of
      Nothing -> sendErrorResponseString "Panel empty"
      Just panels -> do
        case Safe.atMay panels panelIdx of
          Nothing -> sendErrorResponseString "Can't find panel at index"
          Just panel -> do
            case menuPanelCarr panel of
              Nothing -> sendErrorResponseString "Can't find panel at index"
              Just contentArray -> do
                case Safe.atMay (contentArrayArr contentArray) widgetIdx of
                  Nothing -> sendErrorResponseString "Can't find panel at index"
                    Just cObj -> sendSuccessReponse cObj

You don't have to understand what it does to know that is ugly. What is this? JavaScript?!

One thing to look for when you have this mess of nesting is to check if it is returning the same type. For example, I am always returning an error message on Nothing. Although the type here is IO (), if you squint hard enough what I intend to return is the error string.

We also note that all those nesting are resultant of Maybe, and we know Maybe is a Monad. It would be nice if we can somehow take that into advantage.

example :: Maybe String
example = do
  x <- getMaybeX
  y <- getMaybeY
  concat [show x, show y]

If x is Nothing that will short-circuit the entire checks and return Nothing, that is the same idea as case splitting on Maybe with our terrible nesting example at the top.

With the help of monad transformers (since we are working in IO monad at this point), we could make use of Control.Monad.Trans.Either to clean up our codes. Below works pretty much the same as the above except that we are operating in Either monad.

result <- runEitherT . hoistEither $ do
  mPanels     <- maybe (Left "Can't find dashboard") (Right . dashboardPanels . entityVal) mDash
  mPanel      <- maybe (Left "No panels") (\panels -> Right $ Safe.atMay panels panelIdx) mPanels
  mContents   <- maybe (Left "Can't find panel") (Right . menuPanelCarr) mPanel
  mContentObj <- maybe (Left "No contents") (\contents -> Right $ Safe.atMay (contentArrayArr contents) widgetIdx) mContents
  maybe (Left "Can't find widget") Right mContentObj

case result of
  Left err   -> sendErrorResponseString err
  Right cObj -> sendSuccessResponse cObj

Instead of running an action on Nothing, we simply return Left String result so that we can return error message at the point of failure.