如何用ExceptT代替大量IO(要么ab)

GTF*_*GTF 4 error-handling monads haskell monad-transformers

我有一个连接到数据库然后运行查询的函数。每个步骤都会产生IO (Either SomeErrorType SomeResultType).

在学习 Haskell 时,我真正喜欢使用 monad 和类似 monad 的原因之一Either是能够使用 monad 函数(例如>>=和 组合器)mapLeft来简化对预期错误状态的大量处理。

通过阅读博客文章、Control.Monad.Trans文档和其他关于 SO 的答案,我的期望是我必须以某种方式使用转换器/电梯从一个IO上下文移动到另一个Either上下文。

这个答案特别好,但我正在努力将其应用到我自己的案例中。

我的代码的一个更简单的示例:

simpleVersion :: Integer -> Config -> IO ()
simpleVersion id c = 
  connect c >>= \case 
      (Left e)     -> printErrorAndExit e
      (Right conn) -> (run . query id $ conn)
              >>= \case 
                    (Left e)  -> printErrorAndExit e
                    (Right r) -> print r
                                   >> release conn
Run Code Online (Sandbox Code Playgroud)

ExceptT我的问题是(a)我并没有真正理解如何让我到达与世界相似的地方的机制mapLeft handleErrors $ eitherErrorOrResult >>= someOtherErrorOrResult >>= print;(b) 我不确定如何确保连接始终以最好的方式释放(即使在上面的简单示例中),尽管我想我会使用方括号模式

我确信每个(相对)新的 Haskeller 都这么说,但我仍然真的不理解 monad 转换器,而且我读到的所有内容(除了前面链接的 SO 答案)对我来说都太不透明了(还)。

如何将上面的代码转换为删除所有这些嵌套和错误处理的代码?

Dan*_*ner 7

我认为查看以下实例的来源非常有启发MonadExceptT

newtype ExceptT e m a = ExceptT (m (Either e a))

instance (Monad m) => Monad (ExceptT e m) where
    return a = ExceptT $ return (Right a)
    m >>= k = ExceptT $ do
        a <- runExceptT m
        case a of
            Left e -> return (Left e)
            Right x -> runExceptT (k x)
Run Code Online (Sandbox Code Playgroud)

如果忽略newtype包装和展开,它会变得更简单:

m >>= k = do
    a <- m
    case a of
        Left e -> return (Left e)
        Right x -> k x
Run Code Online (Sandbox Code Playgroud)

或者,正如您似乎不喜欢使用do

m >>= k = m >>= \a -> case a of
    Left e -> return (Left e)
    Right x -> k x
Run Code Online (Sandbox Code Playgroud)

你觉得那段代码很熟悉吗?它和您的代码之间的唯一区别是您编写printErrorAndExit而不是return . Left!因此,让我们将其移至printErrorAndExit顶层,并高兴地暂时记住错误而不是打印它。

simpleVersion :: Integer -> Config -> IO (Either Err ())
simpleVersion id c = connect c >>= \case (Left e)     -> return (Left e)
                                         (Right conn) -> (run . query id $ conn)
                                                          >>= \case (Left e)  -> return (Left e)
                                                                    (Right r) -> Right <$> (print r
                                                          >> release conn)
Run Code Online (Sandbox Code Playgroud)

除了我所说的更改之外,您还必须Right <$>在末尾添加一个以从一个IO ()动作转换为一个IO (Either Err ())动作。(稍后会详细介绍这一点。)

好的,让我们尝试ExceptT用上面的绑定替换上面的IO绑定。我将添加一个'来区分ExceptT版本和IO版本(例如>>=' :: IO (Either Err a) -> (a -> IO (Either Err b)) -> IO (Either Err b))。

simpleVersion id c = connect c >>=' \conn -> (run . query id $ conn)
                                             >>=' \r -> Right <$> (print r
                                             >> {- IO >>! -} release conn)
Run Code Online (Sandbox Code Playgroud)

这已经是一个进步了,一些空白的改变让它变得更好。我还将包含一个do版本。

simpleVersion id c =
    connect c >>=' \conn ->
    (run . query id $ conn) >>=' \r ->
    Right <$> (print r >> release conn)

simpleVersion id c = do
    conn <- connect c
    r <- run . query id $ conn
    Right <$> (print r >> release conn)
Run Code Online (Sandbox Code Playgroud)

对我来说,这看起来很干净!当然,在 中main,您仍然需要printErrorAndExit,如:

main = do
    v <- runExceptT (simpleVersion 0 defaultConfig)
    either printErrorAndExit pure v
Run Code Online (Sandbox Code Playgroud)

现在,关于这个Right <$> (...)......我说我想从 转换IO aIO (Either Err a)。嗯,这种事情就是MonadTrans类存在的原因;让我们看看它的实现ExceptT

instance MonadTrans (ExceptT e) where
    lift = ExceptT . liftM Right
Run Code Online (Sandbox Code Playgroud)

嗯,liftM(<$>)是相同的函数,但名称不同。因此,如果我们忽略newtype包装和展开,我们会得到

lift m = Right <$> m
Run Code Online (Sandbox Code Playgroud)

!所以:

simpleVersion id c = do
    conn <- connect c
    r <- run . query id $ conn
    lift (print r >> release conn)
Run Code Online (Sandbox Code Playgroud)

liftIO如果您愿意,也可以选择使用。不同之处在于,lift总是通过一个变压器提升一元动作,但适用于任何一对包裹类型和变压器类型;while通过 monad 变压器堆栈所需的尽可能多的变压器提升动作,但仅适用于liftIO动作。IOIO

当然,到目前为止我们已经省略了所有的newtype包装和展开。为了simpleVersion像我们最后一个示例中一样漂亮,您需要更改connectrun适当地包含这些包装器。