Supporting both MonadBaseControl and UnliftIO ecosystems

To implement general with{SomeResource} functions, I know of two main ecosystems: MonadBaseControl and UnliftIO. (Are there others?)

What are your opinions of a library that

  1. Has .UnliftIO and .MonadBaseControl modules, providing with{SomeResource} convenience functions for both ecosystems? The drawback is that the library depends on both lifted-base and unliftio.
  2. Instead of 1, it avoids those dependencies, gives users only the raw “open” and “close” functions, and tells users to roll out their own with{SomeResource} convenience functions by using the “brackets” of their own choice (classic IO, MonadBaseControl, or UnliftIO).
1 Like

Personally I completely missed the MonadBaseControl bus and have been using unliftio for everything. Simpler API, more basic libraries out of the box in the unliftio package, easy to extend.

What’s your requirement for wrapping both?

4 Likes

I haven’t seen #1 in practice, but I have seen a hybrid approach: orville/orville-postgresql/src/Orville/PostgreSQL/Internal/MonadOrville.hs at 9f28093f6b9b31b54778cda74a3dd5887acf8cb4 · flipstone/orville · GitHub

The library offers its own class for unlifted operations, which can be implemented by either monad-control or unliftio. It offers an unliftio implementation as a convenience.

Yes, the exceptions package, in particular the MonadMask typeclass. (Haskell Unfolder video.)

Instead of 1, it avoids those dependencies, gives users only the raw “open” and “close” functions, and tells users to roll out their own with{SomeResource}

I believe the library could provide the IO-based with{SomeResource} function as well. Both “unliftio” and (I believe) “monad-control” have functions that can “lift” bracket-like control operations.

1 Like

The drawback is that the library depends on both lifted-base and unliftio.

You could easily work around this drawback by using cabal flags to make the dependencies optional.


I’d also like to note that these options are not complete alternatives to each other and seem to be solutions intended for slightly different situations. For instance, UnliftIO has MonadIO as superclass, and therefore only works on transformer stacks that have an IO inside them somewhere.

MonadMask does not have such a requirement, and therefore can be used in a wider number of situations.
I have not used MonadBaseControl myself (and I agree with @ocramz that the API of UnliftIO seems simpler), but I believe it is also more flexible in that it works with non-IO base monads as well.

2 Likes

Well there is a huge difference between MonadUnliftIO and MonadBaseControl IO. The former is intentionally restricted to stateless transformers (reader and newtype identities). On the other hand MonadBaseControl IO instances are implemented for all common monad transformers, which used without caution can lead to subtle bugs (i.e. unlifting exception handlers drops state modifications). MonadMask in this case follows the latter (it’s in a way a kind of restriction of MonadBaseControl to exception-related operations).

After some experience with both, I’d recommend only using UnliftIO.

2 Likes