Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Avoiding case expression ladder without monad transformers

In Real World Haskell monads are introduced as a way to avoid the code marching off the right of the screen by using the Maybe monad. However what happens when the case expressions include other monads like Either or IO, for example?

I know the ladder could be avoided with monad transformers, but aren't they overkill for just an stylistic issue? Is there another idiomatic and lightweight way to deal with this?

Update: Here is an example:

stair s = do
    s <- getLine
    case stepMaybe s of
         Nothing -> return ()
         Just s1 -> case stepEither s1 of
                         Left  _  -> return ()
                         Right s2 -> case stepList s2 of
                                          [s3:_] -> case stepMaybe s3 of
                                                         Nothing -> return ()
                                                         Just s4 -> print s4
                                          _      -> return ()
like image 947
Danny Navarro Avatar asked Feb 01 '14 09:02

Danny Navarro


2 Answers

Use the errors library, which was created precisely for this purpose. It allows you to unify diverse failing computations to agree on a common error-handling mechanism.

For example, let's say that you have two computations, one of which fails using Maybe, and the other of which fails using Either String:

safeHead :: [a] -> Maybe a
safeHead as = case as of
    []  -> Nothing
    a:_ -> Just a

safeDivide :: Double -> Double -> Either String Double
safeDivide x y = if y == 0 then Left "Divide by zero" else Right (x / y)

There are two ways we can make these two functions agree on the same error-handling mechanism. The first way is to convert the Either function to a Maybe by suppressing the String error. This is what the hush function does:

-- Provided by the `errors` package
hush :: Either e a -> Maybe a

example1 :: [Double] -> Maybe Double
example1 xs = do
    x <- safeHead xs
    hush (safeDivide 4 x)

Alternatively, we can annotate the Maybe computation with a descriptive String error message if it fails. This is what the note function does:

-- Also provided by the `errors` package:
note :: e -> Maybe a -> Either e a

example2 :: [Double] -> Either String Double
example2 xs = do
    x <- note "Empty list" (safeHead xs)
    safeDivide 4 x

The errors package also includes conversion functions for MaybeT and EitherT, too. This lets you unify all your error-handling machinery to use the target monad of your choice.

Using your stair example, you would simplify this by getting them to agree on MaybeT:

stair = void $ runMaybeT $ do
    s  <- lift getLine
    s4 <- hoistMaybe $ do
        s1 <-           stepMaybe  s
        s2 <- hush    $ stepEither s1
        s3 <- headMay $ stepList   s2
        stepMaybe  s3
    lift $ print s4
like image 108
Gabriella Gonzalez Avatar answered Nov 15 '22 04:11

Gabriella Gonzalez


Yes there is. Use a smaller indentation setting ) Seriously though, the following should do the trick in your simple example.

import Prelude hiding (mapM_)
import Data.Foldable
import Data.Maybe

stair = do
  s <- getLine
  mapM_ print $ 
    stepMaybe s >>=
    either (const Nothing) Just . stepEither >>=
    listToMaybe . stepList >>=
    stepMaybe

For other cases the operations mapM, mapM_, forM, forM_, sequence, sequence_ of Data.Foldable and Data.Traversable would be the sole toolchain for you. That is assuming you have the missing instances for Either.

However I would still recommend considering transformers, since they pretty much are the standard way of dealing with composition of different monads.

like image 20
Nikita Volkov Avatar answered Nov 15 '22 04:11

Nikita Volkov