• Stars
    star
    150
  • Rank 247,323 (Top 5 %)
  • Language
    Haskell
  • License
    Other
  • Created over 7 years ago
  • Updated 10 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

The MonadUnliftIO typeclass for unlifting monads to IO

unliftio

Tests

Provides the core MonadUnliftIO typeclass, a number of common instances, and a collection of common functions working with it. Not sure what the MonadUnliftIO typeclass is all about? Read on!

NOTE This library is young, and will likely undergo some serious changes over time. It's also very lightly tested. That said: the core concept of MonadUnliftIO has been refined for years and is pretty solid, and even though the code here is lightly tested, the vast majority of it is simply apply withUnliftIO to existing functionality. Caveat emptor and all that.

NOTE The UnliftIO.Exception module in this library changes the semantics of asynchronous exceptions to be in the style of the safe-exceptions package, which is orthogonal to the "unlifting" concept. While this change is an improvment in most cases, it means that UnliftIO.Exception is not always a drop-in replacement for Control.Exception in advanced exception handling code. See Async exception safety for details.

Quickstart

  • Replace imports like Control.Exception with UnliftIO.Exception. Yay, your catch and finally are more powerful and safer (see Async exception safety)!
  • Similar with Control.Concurrent.Async with UnliftIO.Async
  • Or go all in and import UnliftIO
  • Naming conflicts: let unliftio win
  • Drop the deps on monad-control, lifted-base, and exceptions
  • Compilation failures? You may have just avoided subtle runtime bugs

Sound 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 uses ReaderT Env IO, not just plain IO. How can I call my readFile function in that context? One way is to manually unwrap the ReaderT data constructor:

myReadFile :: FilePath -> ReaderT Env IO ByteString
myReadFile fp = ReaderT $ \_env -> readFile fp

But having to do this regularly is tedious, and ties our code to a specific monad transformer stack. Instead, many of us would use MonadIO:

myReadFile :: MonadIO m => FilePath -> m ByteString
myReadFile = 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 the ReaderT constructor via:

myWithBinaryFile fp mode inner =
  ReaderT $ \env -> withBinaryFile
    fp
    mode
    (\h -> runReaderT (inner h) env)

I dare you to try and accomplish this with MonadIO and liftIO. It simply can't be done. (If you're looking for the technical reason, it's because IO appears in negative/argument position in withBinaryFile.)

However, with MonadUnliftIO, this is possible:

import Control.Monad.IO.Unlift

myWithBinaryFile
    :: MonadUnliftIO m
    => FilePath
    -> IOMode
    -> (Handle -> m a)
    -> m a
myWithBinaryFile 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, and finally
  • Working with MVars via modifyMVar and similar
  • Using the timeout function
  • Installing callback handlers (e.g., do you want to do logging in a signal handler?).

This also pops up when working with libraries which are monomorphic on IO, even if they could be written more extensibly.

Examples

Reading through the codebase here is likely the best example to see how to use MonadUnliftIO in practice. And for many cases, you can simply add the MonadUnliftIO constraint and then use the pre-unlifted versions of functions (like UnliftIO.Exception.catch). But ultimately, you'll probably want to use 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 in m in IO. Thus the "unlift": it's like liftIO, but the other way around.

Here are some sample typeclass instances:

instance MonadUnliftIO IO where
  withRunInIO inner = inner id

instance 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, and therefore it can use id
  • IdentityT is essentially just wrapping/unwrapping its data constructor, and then recursively calling withRunInIO on the underlying monad.
  • ReaderT is just like IdentityT, but it captures the reader environment when starting.

We can use withRunInIO 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: use withRunInIO to capture a run function, and then call the original function with the user-supplied arguments, applying run as necessary. withRunInIO takes care of invoking unliftIO for us.

We can also use the run function with different types due to withRunInIO 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 the mask function. This function needs to unlift values to be passed into the restore function, and then liftIO the result of the restore function.

mask :: MonadUnliftIO m => ((forall a. m a -> m a) -> m b) -> m b
mask f = withRunInIO $ \run -> Control.Exception.mask $ \restore ->
  run $ f $ liftIO . restore . run

Limitations

Not all monads which can be an instance of MonadIO can be instances of MonadUnliftIO, due to the MonadUnliftIO laws (described in the Haddocks for the typeclass). This prevents instances for a number of classes 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 this approach 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 fully intentional. Trying to unlift actions in stateful monads leads to unpredictable behavior. For a long and exhaustive example of this, see A 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 do catch in StateT?" You almost certainly have. Let's compare this approach with alternatives. (For an older but more thorough rundown of the options, see Exceptions and monad transformers.)

There are really two approaches to this problem:

  • Use a set of typeclasses for the specific functionality we care about. This is the approach taken by the exceptions package with MonadThrow, MonadCatch, and MonadMask. (Earlier approaches include MonadCatchIO-mtl and MonadCatchIO-transformers.)
  • Define a generic typeclass that allows any control structure to be unlifted. This is the approach taken by the monad-control package. (Earlier approaches include monad-peel and neither.)

The first style gives extra functionality in allowing instances that have nothing to do with runtime exceptions (e.g., a MonadCatch instance for Either). This is arguably a good thing. The second style gives extra functionality in allowing more operations to be unlifted (like threading primitives, not supported by the exceptions package).

Another distinction within the generic typeclass family is whether we unlift to just IO, or to arbitrary base monads. For those familiar, this is the distinction between the MonadIO and MonadBase typeclasses.

This package's main objection to all of the above approaches is that they work for too many monads, and provide difficult-to-predict behavior for a number of them (arguably: plain wrong behavior). For example, in lifted-base (built on top of monad-control), the finally operation will discard mutated state coming from the cleanup action, which is usually not what people expect. exceptions has different behavior here, which is arguably better. But we're arguing here that we should disallow all such ambiguity at the type level.

So comparing to other approaches:

monad-unlift

Throwing this one out there now: the monad-unlift library is built on top of monad-control, and uses fairly sophisticated type level features to restrict it to only the safe subset of monads. The same approach is taken by Control.Concurrent.Async.Lifted.Safe in the lifted-async package. Two problems with this:

  • The complicated type level functionality can confuse GHC in some cases, making it difficult to get code to compile.
  • We don't have an ecosystem of functions like lifted-base built on top of it, making it likely people will revert to the less safe cousin functions.

monad-control

The main contention until now is that unlifting in a transformer like StateT is unsafe. This is not universally true: if only one action is being unlifted, no ambiguity exists. So, for example, try :: IO a -> IO (Either e a) can safely be unlifted in StateT, while finally :: IO a -> IO b -> IO a cannot.

monad-control allows us to unlift both styles. In theory, we could write a variant of lifted-base that never does state discards, and let try be more general than finally. In other words, this is an advantage of monad-control over MonadUnliftIO. We've avoided providing any such extra typeclass in this package though, for two reasons:

  • MonadUnliftIO is a simple typeclass, easy to explain. We don't want to complicated matters (MonadBaseControl is a notoriously difficult to understand typeclass). This simplicity is captured by the laws for MonadUnliftIO, which make the behavior of the run functions close to that of the already familiar lift and liftIO.
  • Having this kind of split would be confusing in user code, when suddenly finally is not available to us. We would rather encourage good practices from the beginning.

Another distinction is that monad-control uses the MonadBase style, allowing unlifting to arbitrary base monads. In this package, we've elected to go with MonadIO style. This limits what we can do (e.g., no unlifting to STM), but we went this way because:

  • In practice, we've found that the vast majority of cases are dealing with IO
  • The split in the ecosystem between constraints like MonadBase IO and MonadIO leads to significant confusion, and MonadIO is by far the more common constraints (with the typeclass existing in base)

exceptions

One thing we lose by leaving the exceptions approach is the ability to model both pure and side-effecting (via IO) monads with a single paradigm. For example, it can be pretty convenient to have MonadThrow constraints for parsing functions, which will either return an Either 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 about how the exception was returned in a constraint like (MonadIO m, MonadThrow m)

The latter could be addressed by defining a law such as throwM = liftIO . throwIO. However, we've decided in this library to go the route of encouraging Either return values for pure functions, and using runtime exceptions in IO otherwise. (You're of course free to also return IO (Either e a).)

By losing MonadCatch, we lose the ability to define a generic way to catch exceptions in continuation based monads (such as ConduitM). Our argument here is that those monads can freely provide their own catching functions. And in practice, long before the MonadCatch typeclass existed, conduit provided a catchC function.

In exchange for the MonadThrow typeclass, we provide helper functions to convert Either values to runtime exceptions in this package. And the MonadMask typeclass is now replaced fully by MonadUnliftIO, which like the monad-control case limits which monads we can be working with.

Async exception safety

The safe-exceptions package builds on top of the exceptions package and provides intelligent behavior for dealing with asynchronous exceptions, a common pitfall. This library provides a set of exception handling functions with the same async exception behavior as that library. You can consider this library a drop-in replacement for safe-exceptions. In the future, we may reimplement safe-exceptions to use MonadUnliftIO instead of MonadCatch and MonadMask.

Package split

The unliftio-core package provides just the typeclass with minimal dependencies (just base and transformers). If you're writing a library, we recommend depending on that package to provide your instances. The unliftio package is a "batteries loaded" library providing a plethora of pre-unlifted helper functions. It's a good choice for importing, or even for use in a custom prelude.

Orphans

The unliftio package currently provides orphan instances for types from the resourcet and monad-logger packages. This is not intended as a long-term solution; once unliftio is deemed more stable, the plan is to move those instances into the respective libraries and remove the dependency on them here.

If there are other temporary orphans that should be added, please bring it up in the issue tracker or send a PR, but we'll need to be selective about adding dependencies.

Future questions

  • Should we extend the set of functions exposed in UnliftIO.IO to include things like hSeek?
  • Are there other libraries that deserve to be unlifted here?

More Repositories

1

mezzohaskell

Community-driven book on intermediate Haskell
298
star
2

inline-c

Haskell
284
star
3

terraform-aws-foundation

Establish a solid Foundation on AWS with these modules for Terraform
HCL
204
star
4

safe-exceptions

Safe, consistent, and easy exception handling
Haskell
132
star
5

ide-backend

ide-backend drives the GHC API to build, query, and run your code
Haskell
120
star
6

minghc

DEPRECATED: Windows installer for GHC including msys
Haskell
108
star
7

typed-process

Alternative API for processes, featuring more type safety
Haskell
106
star
8

weigh

Measure allocations of a Haskell functions/values
Haskell
92
star
9

amber

Manage secret values in-repo via public key cryptography
Rust
83
star
10

applied-haskell

80
star
11

pid1

Do signal handling and orphan reaping for Unix PID1 init processes
Haskell
76
star
12

ghc-prof-flamegraph

Haskell
75
star
13

haskell-scratch

Base Docker image which includes minimal shared libraries for GHC-compiled executables
Makefile
67
star
14

http-reverse-proxy

Reverse proxy HTTP requests, either over raw sockets or with WAI
Haskell
54
star
15

rdr2tls

Haskell web service that redirects all traffic from HTTP to HTTPS
Haskell
52
star
16

ghcjs-react

React bindings for GHCJS
JavaScript
48
star
17

odbc

Haskell ODBC binding with SQL Server support
Haskell
44
star
18

streaming-commons

Common lower-level functions needed by various streaming data libraries
Haskell
36
star
19

schoolofhaskell

Haskell
34
star
20

cache-s3

Haskell
34
star
21

halogen-form

Formlets for halogen
PureScript
30
star
22

say

Send textual messages to a Handle in a thread-friendly way
Haskell
29
star
23

optparse-simple

Simple helper functions to work with optparse-applicative
Haskell
29
star
24

haskell-ide

Repo for collaborating on various shared Haskell IDE components.
Haskell
29
star
25

stackage-cli

Haskell
28
star
26

devops-helpers

Devops helper scripts
Shell
28
star
27

wai-middleware-auth

Authentication middleware that secures WAI application
Haskell
23
star
28

safe-decimal

Haskell
22
star
29

pid1-rs

pid1 handling library for proper signal and zombie reaping of the PID1 process
Rust
22
star
30

stackage-view

JavaScript
21
star
31

best-practices

Documentation for best practices followed at FP Complete
21
star
32

inline-c-cpp

Haskell
19
star
33

sift

Sift through Haskell code for analysis purposes
Haskell
18
star
34

schoolofhaskell.com

Haskell
16
star
35

haskell-multi-docker-example

Haskell
15
star
36

mutable-containers

Abstactions and concrete implementations of mutable containers
14
star
37

monad-unlift

Typeclasses for representing monad (transformer) morphisms
Haskell
14
star
38

stack-docker-image-build

Generate Docker images containing additional packages
Haskell
14
star
39

simple-file-mirror

A dumb tool to mirror changes in a directory between hosts
Haskell
12
star
40

inline-c-nag

Haskell
11
star
41

stream

Streaming data library built around first-class stream fusion for high efficiency
Haskell
11
star
42

yesod-ghcjs

Haskell
10
star
43

th-utilities

Collection of useful functions for use with Template Haskell
Haskell
10
star
44

stackage-curator

DEPRECATED Tools for curating Stackage package sets and building reusable package databases
Haskell
10
star
45

rust-aws-devops

A very small DevOps tool to demo using Rust with AWS
Rust
10
star
46

haskell-filesystem

Contains the system-filepath and system-fileio packages
Haskell
9
star
47

fpco-salt-formula

SaltStack
9
star
48

hackage-mirror

8
star
49

wai-middleware-consul

Haskell
8
star
50

stackage-update

Update your package index incrementally (requires git)
Haskell
8
star
51

fsnotify-conduit

Get filesystem notifications as a stream of events
Haskell
8
star
52

packer-windows

The example code repo to accompany the (yet to be released) blog post on building Windows server images using Packer
PowerShell
8
star
53

flush-queue

Haskell
8
star
54

conduit-combinators

Commonly used conduit functions, for both chunked and unchunked data
8
star
55

fuzzcheck

A library for testing monadic code in the spirit of QuickCheck
Haskell
8
star
56

monad-logger-syslog

monad-logger for syslog
Haskell
7
star
57

bootstrap-salt-formula

SaltStack
6
star
58

ghcjs-from-typescript

Haskell
6
star
59

serial-bench

Ridiculously oversimplified serialization benchmark.
HTML
5
star
60

docker-static-haskell

Docker images based on Alpine for compiling static Haskell executables
5
star
61

stackage-upload

A more secure version of cabal upload which uses HTTPS
Haskell
4
star
62

openconnect-gateway

Docker image and helper scripts to run a VPN gateway with OpenConnect
Shell
4
star
63

executable-hash

Provides the SHA1 hash of the program executable
Haskell
4
star
64

stackage-install

Secure download of packages for cabal-install
Haskell
4
star
65

simple-poll

Haskell
4
star
66

alpine-haskell-stack

Just
4
star
67

monad-logger-json

Functions for logging ToJSON instances with monad-logger
Haskell
4
star
68

demos

Haskell
3
star
69

ghc-rc-stackage

Helper repo for testing GHC release candidates against the Stackage package set
Shell
3
star
70

wai-middleware-ldap

Haskell
3
star
71

stackage-sandbox

Haskell
3
star
72

replace-process

Replace the current process with a different executable
Haskell
3
star
73

strict-concurrency

Mirror of strict-concurrency Darcs repository by Don Stewart
Haskell
3
star
74

kube-test-suite

Validation test suite for kubernetes clusters
Haskell
3
star
75

qapla

Haskell
2
star
76

spiderweb

Link check, capture, and serve sites statically
Haskell
2
star
77

fphc

Issue tracker for FP Haskell Center
2
star
78

wai-middleware-crowd

Middleware and utilities for using Atlassian Crowd authentication
Haskell
2
star
79

stackage-dot

Haskell
2
star
80

hauth

Header Authentication Library for Haskell
Haskell
2
star
81

stackage-setup

Haskell
2
star
82

libraries

FP Complete libraries mega-repo
Haskell
2
star
83

snoc-vector

Vectors with cheap append operations
Haskell
2
star
84

helm-charts

FPCO Helm charts
Smarty
2
star
85

ghc-events-time

Haskell
2
star
86

stackage-cabal

Haskell
2
star
87

chunked-data

Typeclasses for dealing with various chunked data representations
2
star
88

build-profile

Haskell
2
star
89

dockerfile-argocd-deploy

Dockerfile for image containing tools for CI jobs to deploy to Kubernetes using ArgoCD
Dockerfile
1
star
90

simple-mega-repo

Haskell
1
star
91

hackage-upload-analyzer

Performs batch analysis of Hackage upload logs, generating an HTML file with the results.
Haskell
1
star
92

text-stream-decode

Streaming decoding functions for UTF encodings.
Haskell
1
star
93

fpco-cereal

Haskell
1
star
94

nrelic

New Relic GCStats metrics (demo)
Haskell
1
star
95

piggies

Haskell
1
star
96

pretty-time

Rendering of time values in various user-friendly formats.
Haskell
1
star
97

soh-static

School of Haskell static archive
HTML
1
star
98

coredump-uploader

Shell
1
star
99

stack-templates

fpco stack templates
1
star
100

rest-client

Haskell
1
star