haskell - 如何使用 exceptT 替换大量 IO(a b)

标签 haskell error-handling monads monad-transformers

我有一个连接到数据库的函数,然后运行查询。这些步骤中的每一个都会导致 IO (Either SomeErrorType SomeResultType)

我真正喜欢在学习 Haskell 时使用 Either 和类似的 monad 的一件事是能够使用像 >>= 这样的 monad 函数和像这样的组合子mapLeft 来简化许多预期错误状态的处理。

我在这里阅读博客文章、Control.Monad.Trans 文档以及关于 SO 的其他答案的期望是,我必须以某种方式使用转换器/升降机从 IO 上下文到 Either 上下文。

This answer特别是真的很好,但我很难将它应用到我自己的案例中。

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

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

我的问题是 (a) 我并没有真正理解 ExceptT 如何将我带到与 mapLeft handleErrors $ anyErrorOrResult >>= someOtherErrorOrResult >>= 类似的地方的机制打印世界; (b) 我不确定如何确保始终以最好的方式释放连接(即使在我上面的简单示例中),尽管我想我会使用 bracket pattern .

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

我怎样才能将上面的代码转换成可以消除所有这些嵌套和错误处理的东西?

最佳答案

我认为查看 Monad 的来源非常有启发性。 ExceptT 的实例:

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)

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

m >>= k = do
    a <- m
    case a of
        Left e -> return (Left e)
        Right x -> k x

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

m >>= k = m >>= \a -> case a of
    Left e -> return (Left e)
    Right x -> k x

你觉得这段代码很眼熟吗?这与您的代码之间的唯一区别是您编写 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)

除了我所说的更改之外,您还必须粘贴 Right <$>最后从 IO () 转换对 IO (Either Err ()) 采取行动行动。 (稍后会详细介绍。)

好的,让我们尝试替换我们的 ExceptT从上面绑定(bind) IO绑定(bind)。我将添加一个 '区分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)

这已经是一种改进,一些空白更改使它变得更好。我还将包括一个 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)

对我来说,这看起来很干净!当然,在 main ,你还是想printErrorAndExit ,如:

main = do
    v <- runExceptT (simpleVersion 0 defaultConfig)
    either printErrorAndExit pure v

现在,关于 Right <$> (...) ...我说我想从 IO a 转换至IO (Either Err a) .好吧,这种事情就是为什么 MonadTrans类存在;让我们看看 ExceptT 的实现:

instance MonadTrans (ExceptT e) where
    lift = ExceptT . liftM Right

好吧,liftM(<$>)是同一个函数,名字不同。所以如果我们忽略 newtype包装和展开,我们得到

lift m = Right <$> m

!所以:

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

您也可以选择使用 liftIO如果你喜欢。区别在于 lift总是通过一个变压器提升一元 Action ,但适用于任何一对包裹类型和变压器类型;而liftIO解除IO为您的 monad 变压器堆栈通过尽可能多的变压器进行操作,但仅适用于 IO行动。

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

关于haskell - 如何使用 exceptT 替换大量 IO(a b),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/69327798/

相关文章:

reactjs - 错误边界不会从 apollo 客户端捕获错误

java - Eclipse 警告 Uncaught Error

parsing - 错误恢复并解析Anltr4中的树

haskell - 如何使用replicateM解决八皇后问题?

c# - 为什么 Task<T> monad 中不包含 CancellationToken?

Haskell 函数模式匹配问题

optimization - GHC 可以可靠地执行哪些优化?

haskell - 克隆羊型应如何构建

haskell - 如何在gtk2hs中获取TextView中的光标位置?

Haskell:脱糖状态 do 表示法