如何避免案例金字塔?
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
中的 whenLeft
和 whenRight
。您的代码可以重写如下:
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
时,计算将抛出错误并停止。
我的代码结构如下例所示。我很确定应该有一种方法可以更理智地构建它。我假设 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
中的 whenLeft
和 whenRight
。您的代码可以重写如下:
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
时,计算将抛出错误并停止。