Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Should I prefer MonadUnliftIO or MonadMask for bracketting like functions?

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?

like image 494
ocharles Avatar asked Sep 26 '17 11:09

ocharles


1 Answers

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:

  • If you want to match the way most people are doing things today, it's probably best to choose MonadMask, it's the most well adopted solution.
  • If you want that goal, but you also need to do a timeout or withMVar or something, use MonadBaseControl.
  • And if you know there's a specific set of monads you need compatibility with, and want compile time guarantees about the correctness of your code vis-a-vis monadic state, use MonadUnliftIO.
like image 149
Michael Snoyman Avatar answered Oct 16 '22 11:10

Michael Snoyman