5

I have code structured like the example below. I'm pretty sure there should be a way to structure it much more sanely. I would assume Either (or Error) monad could help, but I don't know where to start with that. Any pointers to get me going in the right direction?

data Data1 = Data1 { d2Id :: String }
data Data2 = Data2 { d3Id :: String }
data Data3 = Data3 { d4Id :: String }

getData1 :: String -> IO (Either String Data1)
getData2 :: String -> IO (Either String Data2)
getData3 :: String -> IO (Either String Data3)

process :: Data1 -> Data2 -> Data3 -> IO ()

get :: String -> IO ()
get id = do
  r1 <- getData1 id
  case r1 of
    Left err -> print err
    Right d1 -> do
      r2 <- getData2 $ d2Id d1
      case r2 of
        Left err -> print err
        Right d2 -> do
          r3 <- getData3 $ d3Id d2
          case r3 of
            Left err -> print err
            Right d3 -> do
              process d1 d2 d3
Łukasz
  • 35,061
  • 4
  • 33
  • 33

2 Answers2

4

I'm reopening this question because I think it would be helpful to see how to transform this kind of specific code.

We'll need a few imports:

import Control.Monad.Trans
import Control.Monad.Trans.Either

Then transform your get function by applying EitherT to each IO-action which signals an error by returning an Either:

-- get' :: EitherT String IO ()
get' id = do
  d1 <- EitherT $ getData1 id
  d2 <- EitherT $ getData2 (d2Id d1)
  d3 <- EitherT $ getData3 (d3Id d2)
  liftIO $ process d1 d2 d3

Note that we don't use EitherT for process. Instead we use liftIO since process doesn't signal an error.

GHC should be able to infer the type signature so you don't need to provide it.

To run the new version, use runEitherT which will return an Either value in the IO-monad:

doit :: String -> IO ()
doit id = do
  res <- runEitherT (get' id)
  case res of
    Left err -> print err
    Right d  -> return ()
ErikR
  • 51,541
  • 9
  • 73
  • 124
  • Why was the question closed? – Michael Jun 13 '16 at 19:38
  • It was marked as a dup. This kind of question has come up before, but not exactly in this context, so I decided to re-open it and add an answer. – ErikR Jun 13 '16 at 20:58
  • It seems EitherT is not on Stackage, does that mean that the preferred approach is to use Except for such control flow? https://hoogle.haskell.org/?hoogle=EitherT – Lachezar May 26 '20 at 14:49
1

Since support for runEitherT has been dropped in Either-5, I would recommend using whenLeft and whenRight from Data.Either.Combinators. Your code could be rewritten as follows:

get :: String -> IO ()
get id = do
           r1 <- getData1 id
           whenLeft r1 print
           whenRight r1 (\d1 -> do 
                                  r2 <- getData2 $ d2Id d1
                                  whenLeft r2 print
                                  whenRight r2 (d2 -> 
                                                   [rest of code...] )
                         )

Of course, this would lead to messy code, so whenever I do this, I make sure to end the computation whenever a Left is encountered. The Except monad is great for this. Using it, the code would be rewritten as follows:

get :: String -> ExceptT String IO ()
get id = do
           r1 <- liftIO $ getData1 id
           whenLeft r1 throwError 
           r2 <- liftIO $ getData2 $ d2Id (fromRight r1)
           whenLeft r2 throwError
           r3 <- liftIO $ getData3 $ d3Id (fromRight r2)
           whenLeft r3 throwError
           liftIO $ process (fromRight r1) (fromRight r2) (fromRight r3)

This will make the fromRight statements never fail, because whenever r1,r2 or r3 are Left, the computation will throw an error and it will stop there.