unliftio

repository·master·Indexed 41 days ago

https://github.com/fpco/unliftio

A collection of safe, high-performance IO utilities and the MonadUnliftIO typeclass. It allows developers to run complex IO operations, such as exception handling and concurrency, within monad transformer stacks without the pitfalls of monad-control or the limitations of MonadIO. The library provides safer versions of catch and finally via UnliftIO.Exception and concurrency tools via UnliftIO.Async, serving as a more robust alternative to Control.Exception and Control.Concurrent.Async.

Tokens
1.8K
Snippets
3
Records
9
Agent score
50%

What's inside unliftio

  1. Use unliftio-core for MonadUnliftIO typeclass and basic utilities

    master

    The unliftio-core package provides the core MonadUnliftIO typeclass, along with instances for base and transformers, and basic utility functions.

    Note: This is a minimal package. For most use cases, you should use the full unliftio library, which provides a much broader range of functionality and more comprehensive documentation.

  2. What is MonadUnliftIO and how to use it

    master

    The MonadUnliftIO typeclass solves the problem of running computations that require IO (like withBinaryFile or bracket) inside a monad transformer stack (like ReaderT Env IO) without manually unwrapping constructors or being limited by MonadIO.

    While MonadIO allows you to lift IO actions into your monad using liftIO, it cannot handle functions where IO appears in a negative/argument position (e.g., a callback function that expects IO a but you want to provide m a).

    MonadUnliftIO provides the withRunInIO method, which allows you to capture a function (runInIO) that can execute any computation from your monad m as if it were in IO. This is the "unlift" operation.

    import Control.Monad.IO.Unlift
    
    -- Example: Unlifting a function that takes a callback in IO
    myWithBinaryFile
        :: MonadUnliftIO m
        => FilePath
        -> IOMode
        -> (Handle -> m a)
        -> m a
    myWithBinaryFile fp mode inner =
      withRunInIO $ \runInIO ->
      withBinaryFile
        fp
        mode
        (\h -> runInIO (inner h))
  3. Limitations of MonadUnliftIO

    master

    Not all MonadIO instances can be MonadUnliftIO instances. To maintain predictable behavior, MonadUnliftIO is intentionally restricted to monad transformers that do not have state or multiple exit points that would be corrupted by unlifting.

    Supported Transformers:

    • Transformers with no context (e.g., IdentityT, NoLoggingT).
    • Transformers with context but no state (e.g., ReaderT, LoggingT).

    Unsupported Transformers:

    • Transformers using continuations (e.g., ContT, ConduitM, Pipe).
    • Transformers with monadic state (e.g., StateT, WriterT).
    • Transformers with multiple exit points (e.g., ExceptT).

    Attempting to unlift actions in stateful monads leads to unpredictable behavior, such as lost state updates.

  4. Compare unliftio with other monad transformer approaches

    master

    When deciding whether to use unliftio, consider how it compares to other common Haskell patterns for handling control flow and exceptions in monad transformers:

    • vs. exceptions (MonadThrow, MonadCatch, MonadMask): The exceptions package allows modeling both pure (e.g., Either) and side-effecting (IO) monads with one paradigm. unliftio instead encourages using Either for pure functions and runtime exceptions for IO. unliftio replaces MonadMask with MonadUnliftIO to restrict usage to a safer subset of monads.
    • vs. monad-control: monad-control uses a MonadBase style, allowing unlifting to arbitrary base monads (like STM). unliftio uses a MonadIO style, which is more common and simpler, but limits unlifting to IO.
    • vs. monad-unlift: monad-unlift uses complex type-level features to restrict monad-control to a safe subset. unliftio provides a simpler, more approachable typeclass (MonadUnliftIO) that is easier for GHC to compile and easier for developers to understand.

    Key Design Choice: unliftio prioritly avoids the 'state discard' problem found in monad-control (where operations like finally in a StateT transformer might discard mutated state) by disallowing unsafe unlifting at the type level.

  5. Quickstart with unliftio

    master

    To use unliftio in your project, you can follow these patterns to improve safety and simplify your dependency tree:

    • Replace Exception Imports: Swap Control.Exception with UnliftIO.Exception. This provides more powerful and safer versions of catch and finally by adjusting asynchronous exception semantics.
    • Replace Async Imports: Swap Control.Concurrent.Async with UnliftIO.Async.
    • Full Import: Alternatively, you can import UnliftIO to bring in the library's capabilities.
    • Resolve Naming Conflicts: If there are conflicts between standard libraries and unliftio, let unliftio win.
    • Simplify Dependencies: You can often drop dependencies on monad-control, lifted-base, and exceptions when using unliftio.

    Note on Exceptions: UnliftIO.Exception uses semantics similar to the safe-exceptions package. While generally an improvement, it may not be a drop-in replacement for Control.Exception in highly advanced exception-handling code due to how it handles asynchronous exceptions.

  6. Use unliftio as a drop-in replacement for safe-exceptions

    master
    If you are currently using the safe-exceptions package, you can switch to unliftio. This library provides a set of exception handling functions that implement the same intelligent behavior for dealing with asynchronous exceptions, which is a common pitfall in concurrent Haskell code.
  7. Choose between unliftio and unliftio-core

    master

    Depending on whether you are building an application or a library, you should choose the appropriate package:

    • unliftio-core: Use this if you are writing a library. It contains only the core typeclass with minimal dependencies (base and transformers). This keeps your library's dependency tree small.
    • unliftio: Use this if you are writing an application or want a "batteries included" experience. It provides a wide range of pre-unlifted helper functions and is suitable for importing into a custom prelude.
  8. Common patterns for using withRunInIO

    master

    You can use withRunInIO to wrap existing IO functions to work with any MonadUnliftIO instance. This is common for functions like timeout or race.

    Pattern: Wrapping a function like timeout Capture the run function and pass the inner computation to the original IO function.

    Pattern: Higher-rank polymorphism Because the runner provided by withRunInIO is higher-rank polymorphic, you can use it to run computations of different types within the same block.

    -- Wrapping timeout
    timeout :: MonadUnliftIO m => Int -> m a -> m (Maybe a)
    timeout x y = withRunInIO $ \run -> System.Timeout.timeout x $ run y
    
    -- Wrapping race
    race :: MonadUnliftIO m => m a -> m b -> m (Either a b)
    race a b = withRunInIO $ \run -> A.race (run a) (run b)
  9. The MonadUnliftIO typeclass

    master

    The MonadUnliftIO typeclass is the core abstraction of the library. It requires MonadIO and provides a single method: withRunInIO.

    withRunInIO takes a function that accepts a 'runner' (a higher-rank polymorphic function forall a. m a -> IO a) and returns an IO action. This allows you to 'unlift' computations from your monad m into IO safely.

    class MonadIO m => MonadUnliftIO m where
      withRunInIO :: ((forall a. m a -> IO a) -> IO b) -> m b