unliftio
The MonadUnliftIO typeclass for unlifting monads to IO (batteries included)
https://github.com/fpco/unliftio/tree/master/unliftio#readme
LTS Haskell 23.27: | 0.2.25.1 |
Stackage Nightly 2025-07-13: | 0.2.25.1 |
Latest on Hackage: | 0.2.25.1 |
unliftio-0.2.25.1@sha256:813fa756f7b95ac4741cbaecbacde48bec2d2fcbbbef5d32fe5ad02b54cbcaaf,3410
Module documentation for 0.2.25.1
unliftio
Provides the coreMonadUnliftIO
typeclass, a number of commoninstances, and a collection of common functions working with it. Notsure what theMonadUnliftIO
typeclass is all about? Read on!
NOTE TheUnliftIO.Exception
module in this library changes the semantics of asynchronous exceptions to be in the style of thesafe-exceptions
package, which is orthogonal to the “unlifting” concept. While this change is an improvment in most cases, it means thatUnliftIO.Exception
is not always a drop-in replacement forControl.Exception
in advanced exception handling code. SeeAsync exception safety for details.
Quickstart
- Replace imports like
Control.Exception
withUnliftIO.Exception
. Yay, yourcatch
andfinally
are morepowerful and safer (seeAsync exception safety)! - Similar with
Control.Concurrent.Async
withUnliftIO.Async
- Or go all in and import
UnliftIO
- Naming conflicts: let
unliftio
win - Drop the deps on
monad-control
,lifted-base
, andexceptions
- Compilation failures? You may have just avoided subtle runtime bugs
Sounds like magic? It’s not. Keep reading!
Unlifting in 2 minutes
Let’s say I have a function:
readFile :: FilePath -> IO ByteString
But I’m writing code inside a function that usesReaderT Env IO
, notjust plainIO
. How can I call myreadFile
function in thatcontext? One way is to manually unwrap theReaderT
data constructor:
myReadFile :: FilePath -> ReaderT Env IO ByteStringmyReadFile fp = ReaderT $ \_env -> readFile fp
But having to do this regularly is tedious, and ties our code to aspecific monad transformer stack. Instead, many of us would useMonadIO
:
myReadFile :: MonadIO m => FilePath -> m ByteStringmyReadFile = liftIO . readFile
But now let’s play with a different function:
withBinaryFile :: FilePath -> IOMode -> (Handle -> IO a) -> IO a
We want a function with signature:
myWithBinaryFile :: FilePath -> IOMode -> (Handle -> ReaderT Env IO a) -> ReaderT Env IO a
If I squint hard enough, I can accomplish this directly with theReaderT
constructor via:
myWithBinaryFile fp mode inner = ReaderT $ \env -> withBinaryFile fp mode (\h -> runReaderT (inner h) env)
I dare you to try and accomplish this withMonadIO
andliftIO
. It simply can’t be done. (If you’re looking for thetechnical reason, it’s becauseIO
appears innegative/argument positioninwithBinaryFile
.)
However, withMonadUnliftIO
, this is possible:
import Control.Monad.IO.UnliftmyWithBinaryFile :: MonadUnliftIO m => FilePath -> IOMode -> (Handle -> m a) -> m amyWithBinaryFile fp mode inner = withRunInIO $ \runInIO -> withBinaryFile fp mode (\h -> runInIO (inner h))
That’s it, you now know the entire basis of this library.
How common is this problem?
This pops up in a number of places. Some examples:
- Proper exception handling, with functions like
bracket
,catch
,andfinally
- Working with
MVar
s viamodifyMVar
and similar - Using the
timeout
function - Installing callback handlers (e.g., do you want to dologging in a signalhandler?).
This also pops up when working with libraries which are monomorphic onIO
, even if they could be written more extensibly.
Examples
Reading through the codebase here is likely the best example to seehow to useMonadUnliftIO
in practice. And for many cases, you cansimply add theMonadUnliftIO
constraint and then use thepre-unlifted versions of functions (likeUnliftIO.Exception.catch
). But ultimately, you’ll probably want touse the typeclass directly. The type class has only one method –withRunInIO
:
class MonadIO m => MonadUnliftIO m where withRunInIO :: ((forall a. m a -> IO a) -> IO b) -> m b
withRunInIO
provides a function to run arbitrary computations inm
inIO
. Thus the “unlift”: it’s likeliftIO
, but the other way around.
Here are some sample typeclass instances:
instance MonadUnliftIO IO where withRunInIO inner = inner idinstance MonadUnliftIO m => MonadUnliftIO (ReaderT r m) where withRunInIO inner = ReaderT $ \r -> withRunInIO $ \run -> inner (run . flip runReaderT r)instance MonadUnliftIO m => MonadUnliftIO (IdentityT m) where withRunInIO inner = IdentityT $ withRunInIO $ \run -> inner (run . runIdentityT)
Note that:
- The
IO
instance does not actually do any lifting or unlifting, andtherefore it can useid
IdentityT
is essentially just wrapping/unwrapping its dataconstructor, and then recursively callingwithRunInIO
on theunderlying monad.ReaderT
is just likeIdentityT
, but it captures the readerenvironment when starting.
We can usewithRunInIO
to unlift a function:
timeout :: MonadUnliftIO m => Int -> m a -> m (Maybe a)timeout x y = withRunInIO $ \run -> System.Timeout.timeout x $ run y
This is a common pattern: usewithRunInIO
to capture a run function,and then call the original function with the user-supplied arguments,applyingrun
as necessary.withRunInIO
takes care of invokingunliftIO
for us.
We can also use the run function with different types due towithRunInIO
being higher-rank polymorphic:
race :: MonadUnliftIO m => m a -> m b -> m (Either a b)race a b = withRunInIO $ \run -> A.race (run a) (run b)
And finally, a more complex usage, when unlifting themask
function. This function needs to unlift values to be passed into therestore
function, and thenliftIO
the result of therestore
function.
mask :: MonadUnliftIO m => ((forall a. m a -> m a) -> m b) -> m bmask f = withRunInIO $ \run -> Control.Exception.mask $ \restore -> run $ f $ liftIO . restore . run
Limitations
Not all monads which can be an instance ofMonadIO
can be instancesofMonadUnliftIO
, due to theMonadUnliftIO
laws (described in theHaddocks for the typeclass). This prevents instances for a number ofclasses of transformers:
- Transformers using continuations (e.g.,
ContT
,ConduitM
,Pipe
) - Transformers with some monadic state (e.g.,
StateT
,WriterT
) - Transformers with multiple exit points (e.g.,
ExceptT
and its ilk)
In fact, there are two specific classes of transformers that thisapproach does work for:
- Transformers with no context at all (e.g.,
IdentityT
,NoLoggingT
) - Transformers with a context but no state (e.g.,
ReaderT
,LoggingT
)
This may sound restrictive, but this restriction is fullyintentional. Trying to unlift actions in stateful monads leads tounpredictable behavior. For a long and exhaustive example of this, seeA Tale of Two Brackets,which was a large motivation for writing this library.
Comparison to other approaches
You may be thinking “Haven’t I seen a way to docatch
inStateT
?”You almost certainly have. Let’s compare this approach withalternatives. (For an older but more thorough rundown of the options,seeExceptions and monad transformers.)
There are really two approaches to this problem:
- Use a set of typeclasses for the specific functionality we careabout. This is the approach taken by the
exceptions
package withMonadThrow
,MonadCatch
, andMonadMask
. (Earlier approachesincludeMonadCatchIO-mtl
andMonadCatchIO-transformers
.) - Define a generic typeclass that allows any control structure to beunlifted. This is the approach taken by the
monad-control
package. (Earlier approaches includemonad-peel
andneither
.)
The first style gives extra functionality in allowing instances thathave nothing to do with runtime exceptions (e.g., aMonadCatch
instance forEither
). This is arguably a good thing. The secondstyle gives extra functionality in allowing more operations to beunlifted (like threading primitives, not supported by theexceptions
package).
Another distinction within the generic typeclass family is whether weunlift to justIO
, or to arbitrary base monads. For those familiar,this is the distinction between theMonadIO
andMonadBase
typeclasses.
This package’s main objection to all of the above approaches is thatthey work for too many monads, and provide difficult-to-predictbehavior for a number of them (arguably: plain wrong behavior). Forexample, inlifted-base
(built on top ofmonad-control
), thefinally
operation will discard mutated state coming from the cleanupaction, which is usually not what people expect.exceptions
hasdifferent behavior here, which is arguably better. But we’re arguinghere that we should disallow all such ambiguity at the type level.
So comparing to other approaches:
monad-unlift
Throwing this one out there now: themonad-unlift
library is builton top ofmonad-control
, and uses fairly sophisticated type levelfeatures to restrict it to only the safe subset of monads. The sameapproach is taken byControl.Concurrent.Async.Lifted.Safe
in thelifted-async
package. Two problems with this:
- The complicated type level functionality can confuse GHC in somecases, making it difficult to get code to compile.
- We don’t have an ecosystem of functions like
lifted-base
built ontop of it, making it likely people will revert to the less safecousin functions.
monad-control
The main contention until now is that unlifting in a transformer likeStateT
is unsafe. This is not universally true: if only one actionis being unlifted, no ambiguity exists. So, for example,try :: IO a -> IO (Either e a)
can safely be unlifted inStateT
, whilefinally :: IO a -> IO b -> IO a
cannot.
monad-control
allows us to unlift both styles. In theory, we couldwrite a variant oflifted-base
that never does state discards, andlettry
be more general thanfinally
. In other words, this is anadvantage ofmonad-control
overMonadUnliftIO
. We’ve avoidedproviding any such extra typeclass in this package though, for tworeasons:
MonadUnliftIO
is a simple typeclass, easy to explain. We don’twant to complicated matters (MonadBaseControl
is a notoriouslydifficult to understand typeclass). This simplicityis captured by the laws forMonadUnliftIO
, which make thebehavior of the run functions close to that of the already familiarlift
andliftIO
.- Having this kind of split would be confusing in user code, whensuddenly
finally
is not available to us. We would rather encouragegood practicesfrom the beginning.
Another distinction is thatmonad-control
uses theMonadBase
style, allowing unlifting to arbitrary base monads. In this package,we’ve elected to go withMonadIO
style. This limits what we can do(e.g., no unlifting toSTM
), but we went this way because:
- In practice, we’ve found that the vast majority of cases are dealingwith
IO
- The split in the ecosystem between constraints like
MonadBase IO
andMonadIO
leads to significant confusion, andMonadIO
is byfar the more common constraints (with the typeclass existing inbase
)
exceptions
One thing we lose by leaving theexceptions
approach is the abilityto model both pure and side-effecting (viaIO
) monads with a singleparadigm. For example, it can be pretty convenient to haveMonadThrow
constraints for parsing functions, which will eitherreturn anEither
value or throw a runtime exception. That said,there are detractors of that approach:
- You lose type information about which exception was thrown
- There is ambiguity abouthow the exception was returned in aconstraint like
(MonadIO m, MonadThrow m
)
The latter could be addressed by defining a law such asthrowM = liftIO . throwIO
. However, we’ve decided in this library to go theroute of encouragingEither
return values for pure functions, andusing runtime exceptions inIO
otherwise. (You’re of course free toalso returnIO (Either e a)
.)
By losingMonadCatch
, we lose the ability to define a generic way tocatch exceptions in continuation based monads (such asConduitM
). Our argument here is that those monads can freely providetheir own catching functions. And in practice, long before theMonadCatch
typeclass existed,conduit
provided acatchC
function.
In exchange for theMonadThrow
typeclass, we provide helperfunctions to convertEither
values to runtime exceptions in thispackage. And theMonadMask
typeclass is now replaced fully byMonadUnliftIO
, which like themonad-control
case limits whichmonads we can be working with.
Async exception safety
Thesafe-exceptions
package builds on top of theexceptions
package and provides intelligent behavior for dealing withasynchronous exceptions, a common pitfall. This library provides a setof exception handling functions with the same async exception behavioras that library. You can consider this library a drop-in replacementforsafe-exceptions
. In the future, we may reimplementsafe-exceptions
to useMonadUnliftIO
instead ofMonadCatch
andMonadMask
.
Package split
Theunliftio-core
package provides just the typeclass with minimaldependencies (justbase
andtransformers
). If you’re writing alibrary, we recommend depending on that package to provide yourinstances. Theunliftio
package is a “batteries included” libraryproviding a plethora of pre-unlifted helper functions. It’s a goodchoice for importing, or even for use in a custom prelude.
Orphans
Theunliftio
package currently provides orphan instances for typesfrom theresourcet
andmonad-logger
packages. This is not intendedas a long-term solution; onceunliftio
is deemed more stable, theplan is to move those instances into the respective libraries andremove the dependency on them here.
If there are other temporary orphans that should be added, pleasebring them up in the issue tracker or send a PR, but we’ll need to beselective about adding dependencies.
Future questions
- Should we extend the set of functions exposed in
UnliftIO.IO
to includethings likehSeek
? - Are there other libraries that deserve to be unlifted here?
Changes
Changelog for unliftio
0.2.25.1
- Forward compatibility with
-Wnoncanonical-monoid-instances
becoming an error
0.2.25.0
- Add
UnliftIO.Exception.Lens
0.2.24.0
- Add
UnliftIO.STM.writeTMVar
- Add
UnliftIO.STM.stateTVar
0.2.23.0
UnliftIO.Exception
re-exports theHandler
and sync/async exception wrappersfromsafe-exceptions
, instead of redefining them.- With this change, you won’t be able to distinguish between an asynchronousexception from
UnliftIO.Exception.throwTo
andControl.Exception.Safe.throwTo
. - #103
- With this change, you won’t be able to distinguish between an asynchronousexception from
0.2.22.0
- Add
UnliftIO.STM.flushTBQueue
- Add
UnliftIO.STM.lengthTBQueue
0.2.21.0
- Add
UnliftIO.Directory.createDirectoryLink
- Add
UnliftIO.Directory.removeDirectoryLink
- Add
UnliftIO.Directory.getSymbolicLinkTarget
- Add
UnliftIO.Directory.XdgDirectoryList
- Add
UnliftIO.Directory.getXdgDirectoryList
0.2.20.1
- Fix time-osx.c for aarch64 mac#91
0.2.20
- Add lifted
System.IO.openFile
(https://github.com/fpco/unliftio/pull/88)
0.2.19
- Add
Eq
instance forStringException
(https://github.com/fpco/unliftio/pull/83)
0.2.18
- Reexport
asyncExceptionFromException
andasyncExceptionToException
#81
0.2.17
- Re-export
AsyncCancelled
inUnliftIO.Async
#80 - Add
fromExceptionUnwrap
#80 - Add
catchSyncOrAsync
,handleSyncOrAsync
, andtrySyncOrAsync
#80
0.2.16
- Add
createFileLink
0.2.15
- Updated documentation mentioning that
MonadUnliftIO
may be derived usingthenewtype
strategy#72 - Add
mapExceptionM
#75
0.2.14
- Add
UnliftIO.QSem
- Add
UnliftIO.QSemN
0.2.13.1
- Improve
UnliftIO.Exception
documentation
0.2.13
- Add
UnliftIO.STM.orElse
- Re-export all of
SeekMode
0.2.12.1
- Minor doc improvements
0.2.12
Dropped support for ghc-7.8
Addition of
UnliftIO.IO.File
module and atomic+durable file writes:writeBinaryFile
writeBinaryFileAtomic
writeBinaryFileDurable
writeBinaryFileDurableAtomic
withBinaryFileAtomic
withBinaryFileDurable
withBinaryFileDurableAtomic
ensureFileDurable
0.2.11
- Deprecate
forkWithUnmask
in favor of the newly addedforkIOWithUnmask
toimprove consistency. [https://github.com/fpco/unliftio/issues/44]
0.2.10
- Add pooling related functions for unliftio
0.2.9.0
- Add the new
Conc
datatype as a more efficient alternative toConcurrently
0.2.8.1
- Support for
stm-2.5.0.0
0.2.8.0
- Add ‘UnliftIO.Memoize’
0.2.7.1
- Minor doc improvements
0.2.7.0
- Re-export
tryPutTMVar
fromUnliftIO.STM
0.2.6.0
- Add
UnliftIO.Directory
0.2.5.0
- Add
UnliftIO.Environment
/UnliftIO.Foreign
/UnliftIO.Process
0.2.4.0
- Use more generalized
withRunInIO
inunliftio-core-0.1.1.0
- Add
getMonotonicTime
function
0.2.2.0
- Add
pureTry
andpureTryDeep
0.2.1.0
- Add
UnliftIO.STM
- Add a number of functions to
UnliftIO.IO
0.2.0.0
- Remove
monad-logger
instances (moved intomonad-logger
itself inrelease0.3.26
) - Remove
resourcet
instances andUnliftIO.Resource
(moved intoresourcet
itself in release1.1.10
)
0.1.1.0
0.1.0.0
- Initial release.