我目前正在构建一个新的API,它当前提供的功能之一是:

inSpan :: Tracer -> Text -> IO a -> IO a

我想将Tracer移至monad,使我的签名更像
inSpan :: MonadTracer m => Text -> m a -> m a
inSpan的实现使用bracket,这意味着我有两个主要选择:
class MonadUnliftIO m => MonadTracer m

要么
class MonadMask m => MonadTracer m

但是我该选哪个呢?请注意,我可以控制我提到的所有类型,这使我稍微倾向于MonadMask,因为它没有在底部强制执行IO(也就是说,我们可能有一个纯MonadTracer实例)。

还有什么我应该考虑的吗?

最佳答案

让我们先列出选项(在此过程中重复一些问题):
MonadMask库中的

  • exceptions。这可以在各种monad和转换器上使用,并且不需要基本monad为IO
  • MonadUnliftIO(或unliftio-core)库中的
  • unliftio。该库仅适用于以IO为基础的单子(monad),并且在某种程度上与ReaderT env IO同构。
  • MonadBaseControl库中的
  • monad-control。该库的底部需要IO,但允许使用非ReaderT。

  • 现在进行权衡。 MonadUnliftIO是最新版本,并且对库的支持最少。这意味着,除了单例可以成为实例的局限性之外,还没有编写许多好的实例。

    重要的问题是:为什么MonadUnliftIO对类似ReaderT的东西提出了看似任意的要求?这是为了防止丢失单子(monad)状态的问题。例如,bracket_ (put 1) (put 2) (put 3)的语义并不清楚,因此MonadUnliftIO不允许StateT实例。
    MonadBaseControl放宽了ReaderT的限制,并具有更广泛的库支持。内部它也被认为比其他两个更复杂,但是对于您的用法来说,这并不重要。并且它使您能够如上所述出错于单子(monad)状态。如果您在使用时很小心,那将无关紧要。
    MonadMask允许使用完全纯净的变压器堆栈。我认为围绕纯堆栈中的异步异常进行建模的有用性值得商argument,但我知道这种方法有时是人们想要做的。为了换取更多实例,您仍然对单子(monad)状态有所限制,并且无法取消某些IO控制动作,例如timeoutforkIO

    我的建议:
  • 如果您想匹配当今大多数人的工作方式,最好选择MonadMask,这是最常用的解决方案。
  • 如果您要实现此目标,但还需要执行timeoutwithMVar或其他操作,请使用MonadBaseControl
  • 如果您知道有一组特定的monads需要与之兼容,并且想要保证代码相对于monadic状态的正确性,请使用MonadUnliftIO
  • 10-06 05:13