如何避免案例金字塔?

How to avoid pyramid of cases?

我的代码结构如下例所示。我很确定应该有一种方法可以更理智地构建它。我假设 Either (or Error) monad 可以提供帮助,但我不知道从哪里开始。有什么指示可以让我朝着正确的方向前进吗?

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

我重新提出这个问题,因为我认为看看它会有所帮助 这种具体代码怎么改造

我们需要一些导入:

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

然后通过将 EitherT 应用于每个 IO 操作来转换您的 get 函数,该操作通过 returning 一个 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

请注意,我们不使用 EitherT 作为 process。相反,我们使用 liftIO 因为 process 不表示错误。

GHC 应该能够推断出类型签名,因此您无需提供它。

到运行新版本,使用runEitherT这将return IO-monad中的Either值:

doit :: String -> IO ()
doit id = do
  res <- runEitherT (get' id)
  case res of
    Left err -> print err
    Right d  -> return ()

由于 Either-5 中不再支持 runEitherT,因此我建议使用 Data.Either.Combinators 中的 whenLeftwhenRight。您的代码可以重写如下:

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...] )
                         )

当然,这会导致代码混乱,所以每当我这样做时,我都会确保在遇到 Left 时结束计算。 Except monad 非常适合这个。使用它,代码将重写如下:

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)

这将使 fromRight 语句永远不会失败,因为每当 r1、r2 或 r3 为 Left 时,计算将抛出错误并停止。