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.