我应该更喜欢 MonadUnliftIO 还是 MonadMask 来像函数一样进行包围?

Should I prefer MonadUnliftIO or MonadMask for bracketting like functions?

我目前正在构建一个新的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 和 transformer 上工作,并且不需要基本 monad 是 IO.
  • MonadUnliftIO 来自 unliftio-core(或 unliftio)库。该库仅适用于基数为 IO 的单子,并且在某种程度上与 ReaderT env IO.
  • 同构
  • MonadBaseControl 来自 monad-control 库。该库将需要 IO 作为基础,但将允许非 ReaderT。

现在权衡。 MonadUnliftIO 是最新加入的,并且具有最不发达的库支持。这意味着,除了 monad 可以作为实例的限制之外,许多好的实例还没有被编写出来。

重要的问题是:为什么 MonadUnliftIO 围绕 ReaderT 之类的事情提出这个看似武断的要求?这是为了防止丢失单子状态的问题。例如,bracket_ (put 1) (put 2) (put 3) 的语义不是很清楚,因此 MonadUnliftIO 不允许 StateT 实例。

MonadBaseControl 放宽了 ReaderT 限制并具有更广泛的库支持。它在内部也被认为比其他两个更复杂,但对于您的使用来说这并不重要。如上所述,它允许您在 monadic 状态上犯错误。如果您在使用时小心,这无关紧要。

MonadMask 允许完全纯变压器组。我认为围绕在纯堆栈中建模异步异常的有用性有一个很好的论据,但我知道这种方法有时是人们想要做的。作为获得更多实例的交换,您仍然对 monadic 状态有限制,并且无法解除某些 IO 控制操作,例如 timeoutforkIO.

我的推荐:

  • 如果你想匹配当今大多数人做事的方式,最好选择MonadMask,这是最常用的解决方案。
  • 如果你想要那个目标,但是你还需要做一个timeoutwithMVar或其他东西,使用MonadBaseControl
  • 如果您知道有一组特定的 monad 需要与之兼容,并且希望编译时保证您的代码相对于 monadic 状态的正确性,请使用 MonadUnliftIO