I'm currently building a new API, and one of the functions it currently provides is:
inSpan :: Tracer -> Text -> IO a -> IO a
I'm looking to move that Tracer
into a monad, giving me a signature more like
inSpan :: MonadTracer m => Text -> m a -> m a
The implementation of inSpan
uses bracket
, which means I have two main options:
class MonadUnliftIO m => MonadTracer m
or
class MonadMask m => MonadTracer m
But which should I prefer? Note that I'm in control of all the types I've mentioned, which makes me slightly lean towards MonadMask
as it doesn't enforce IO
at the bottom (that is, we could perhaps have a pure MonadTracer
instance).
Is there anything else I should consider?
Let's lay out the options first (repeating some of your question in the process):
MonadMask
from the exceptions
library. This can work on a wide range of monads and transformers, and does not require that the base monad be IO
.MonadUnliftIO
from the unliftio-core
(or unliftio
) library. This library only works for monads with IO
at their base, and which is somehow isomorphic to ReaderT env IO
.MonadBaseControl
from the monad-control
library. This library will require IO
at the base, but will allow non-ReaderT.Now the tradeoffs. MonadUnliftIO
is the newest addition to the fray, and has the least developed library support. This means that, in addition to the limitations of what monads can be instances, many good instances just haven't been written yet.
The important question is: why does MonadUnliftIO
make this seemingly arbitrary requirement around ReaderT
-like things? This is to prevent issues with lost monadic state. For example, the semantics of bracket_ (put 1) (put 2) (put 3)
are not really clear, and therefore MonadUnliftIO
disallows a StateT
instance.
MonadBaseControl
relaxes the ReaderT
restriction and has wider library support. It's also considered more complicated internally than the other two, but for your usages that shouldn't really matter. And it allows you to make mistakes with the monadic state as mentioned above. If you're careful in your usage, this won't matter.
MonadMask
allows totally pure transformer stacks. I think there's a good argument to be had around the usefulness of modeling asynchronous exceptions in a pure stack, but I understand this kind of approach is something people want to do sometimes. In exchange for getting more instances, you still have the limitations around monadic state, plus the inability to lift some IO
control actions, like timeout
or forkIO
.
My recommendation:
MonadMask
, it's the most well adopted solution.timeout
or withMVar
or something, use MonadBaseControl
.MonadUnliftIO
.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With