在像 ExceptT a IO 这样的 monad 堆栈中管理资源的最佳方法是什么?

What is the best way to manage resources in a monad stack like ExceptT a IO?

无论好坏,Haskell 的流行 Servant library has made it common-place to run code in a monad transformer stack involving ExceptT err IO. Servant's own handler monad is ExceptT ServantErr IO. As many argue, this is a somewhat troublesome monad 工作因为有 多种方法无法展开 :1) 通过来自基础 IO 的正常异常,或 2) 通过返回 Left.

作为 Ed Kmett 的 exceptions library helpfully clarifies:

Continuation-based monads, and stacks such as ErrorT e IO which provide for multiple failure modes, are invalid instances of this [MonadMask] class.

这非常不方便,因为 MonadMask 让我们可以访问有用的 [多态版本] bracket 函数来进行资源管理(不会因异常等而泄漏资源)。但是在 Servant 的 Handler monad 中我们不能使用它。

我不是很熟悉,但有人说解决方案是使用 monad-control 并且它有许多合作伙伴库,如 lifted-baselifted-async 给你的 monad访问 bracket 等资源管理工具(大概这也适用于 ExceptT err IO 和朋友?)。

然而,monad-control 似乎是 losing favor in the community,但我不知道替代方案是什么。甚至 Snoyman 最近的 safe-exceptions 库也使用了 Kmett 的 exceptions 库并避免了 monad-control.

有人可以为像我这样试图认真 Haskell 使用的人澄清当前的故事吗?

您可以在 IO、return 中处理最后一个 IO (Either ServantErr r) 类型的值,然后将其包装在 ExceptT 中以使其适合处理程序类型。这将使您可以在 IO 中正常使用 bracket。这种方法的一个问题是您丢失了 ExceptT 提供的 "automatic error management"。也就是说,如果你在处理程序中间失败,你将不得不在 Either 和类似的事情上执行显式模式匹配。


上面基本上是为ExceptT重新实现了MonadTransControl实例,也就是

instance MonadTransControl (ExceptT e) where
    type StT (ExceptT e) a = Either e a
    liftWith f = ExceptT $ liftM return $ f $ runExceptT
    restoreT = ExceptT

monad-control 在像 bracket 这样的提升函数时工作正常,但它有奇怪的极端情况,具有如下函数(取自 this blog post) :

import Control.Monad.Trans.Control

callTwice :: IO a -> IO a
callTwice action = action >> action

callTwice' :: ExceptT () IO () -> ExceptT () IO ()
callTwice' = liftBaseOp_ callTwice

如果我们向 callTwice' 传递一个打印内容并在

之后立即失败的操作
main :: IO ()
main = do
    let printAndFail = lift (putStrLn "foo") >> throwE ()
    runExceptT (callTwice' printAndFail) >>= print  

它无论如何都会打印两次 "foo",即使我们的直觉告诉它应该在第一次执行操作失败后停止。


另一种方法是使用 resourcet library and work in a ExceptT ServantErr (ResourceT IO) r monad. You would need to use resourcet functions like allocate 而不是 bracket,并在末尾调整 monad,如:

import Control.Monad.Trans.Resource
import Control.Monad.Trans.Except

adapt :: ExceptT ServantErr (ResourceT IO) r -> ExceptT err IO r 
adapt = ExceptT . runResourceT . runExceptT

或喜欢:

import Control.Monad.Morph

adapt' :: ExceptT err (ResourceT IO) r -> ExceptT err IO r 
adapt' = hoist runResourceT

我的建议:让你的代码存在于 IO 而不是 ExceptT 中,并将每个处理函数包装在 ExceptT . try.