effectful

repository·master·Indexed 19 days ago

https://github.com/haskell-effectful/effectful

A high-performance, extensible effects library for Haskell that uses a ReaderT-over-IO approach to provide an efficient alternative to monad transformer stacks. It features the Eff monad, supporting both static and dynamic dispatch, and includes an optional GHC plugin (effectful-plugin) for improved effect disambiguation. The library is designed to resolve common issues found in mtl and transformers, such as state loss during exceptions and space leaks in WriterT implementations.

Tokens
3.2K
Snippets
6
Records
17
Agent score
67%

What's inside effectful

  1. Overview of effectful

    master
    effectful is a high-performance, extensible effects library for Haskell. It is designed to be an enriched version of the ReaderT design pattern, providing an Eff monad that acts as a ReaderT over IO with an extensible environment for effects. It aims to replace complex monad transformer stacks with improved semantics, performance, and usability while maintaining seamless integration with the existing Haskell ecosystem (e.g., exceptions, unliftio-core, resourcet).
  2. Understand the effectful package structure

    master

    The library is distributed across several packages depending on your needs:

    • effectful-core: Contains the core library and basic effects. It has a small dependency footprint and provides the fundamental building blocks.
    • effectful-plugin: An optional GHC plugin to improve effect disambiguation.
    • effectful-th: Provides Template Haskell utilities for generating effect-related boilerplate.
    • effectful: The main package. It re-exports effectful-core and provides most features of the unliftio package divided into appropriate effects.
  3. Understand the effectful benchmark suite

    master

    The effectful benchmark suite compares the performance of various extensible effects libraries across two primary scenarios. These benchmarks are designed to provide realistic performance expectations for real-world applications by using NOINLINE pragmas to prevent GHC from performing whole-program specialization (which would artificially inflate performance).

    Benchmark Scenarios

    • countdown: A microbenchmark measuring the performance of monadic binds and effect dispatch.
    • filesize: A more practical benchmark that includes various operations, including I/O.

    Benchmark Flavors

    To simulate different application architectures, each benchmark is run in two flavors:

    • shallow: Contains only the effects strictly necessary for the benchmark.
    • deep: Contains the necessary effects plus 5 redundant effects (10 total) added before and after the relevant ones. This simulates a typical application where a function only uses a subset of the total effects available in the environment.
  4. Understand the limitations of effectful

    master

    The Eff monad does not support effect handlers that require suspending or capturing the rest of the computation to resume it later (potentially multiple times). Consequently, effectful does not provide:

    • A NonDet effect handler (for executing multiple Alternative branches and collecting results).
    • A Coroutine effect.

    If your application requires these capabilities, it is recommended to use established libraries like conduit or list-t alongside effectful.

  5. The risks of ExceptT with resource management

    master

    Using ExceptT for error handling can lead to resource leaks because ExceptT errors are not runtime exceptions. Standard resource management functions like onException from the exceptions library do not catch ExceptT errors, meaning cleanup code may never execute when a throwError occurs.

    To avoid this in mtl, one must use onError instead of onException, but effectful solves this by making Error effects behave like runtime exceptions, ensuring standard resource management tools work correctly.

    -- Example of the failure pattern in mtl:
    -- If 'throwError' is called, 'releaseResourceOnFailure' is skipped
    -- because 'onException' only looks for runtime exceptions.
    test . runExceptT @String . withResource $ \Resource -> throwError "oops"
  6. Why Strict WriterT should be avoided

    master

    The strict Control.Monad.Trans.Writer.Strict implementation is considered unusable because it always leaks space. It fails to provide the benefits of strictness due to two primary reasons:

    1. Non-tail recursive bind: The bind operation is not tail recursive, and strict pattern matches force the computation of the continuation k even if the bind of m is lazy.
    2. Thunk accumulation: The expression w mappend w' is never actually evaluated, leading to a massive accumulation of thunks in memory.
  7. How the Eff monad works

    master

    The core abstraction is the Eff monad. Unlike traditional monad transformer stacks, Eff is a concrete monad that functions like a ReaderT over IO on steroids. This design provides several benefits:

    • Performance: Because Eff is concrete, GHC can optimize it effectively without requiring explicit INLINE pragmas.
    • Interoperability: Being a reader allows for seamless integration with ubiquitous classes like MonadBaseControl and MonadUnliftIO.
    • Correctness: It provides correct semantics in the presence of runtime exceptions, preventing discarded state updates.
    • Dispatch Modes: It supports both statically dispatched effects (determined at compile time) and dynamically dispatched effects (determined at runtime).
  8. Understanding the pitfalls of Lazy WriterT

    master

    The lazy Control.Monad.Trans.Writer.Lazy implementation is only efficient in niche scenarios where both the underlying monad m is lazy (e.g., Identity, but not IO) and the accumulated value w can be produced and consumed lazily (e.g., [a], but not Sum Int). If these conditions are not met, the implementation will leak space.

    When it works (Constant Space):

    • m is lazy and w is a lazy structure like a list.

    When it leaks space:

    • If m is strict (like IO).
    • If w is a strict structure (like Sum Int).
    -- Example of efficient usage with lazy production/consumption
    import Control.Monad.Trans.Writer.Lazy
    import Data.Foldable
    
    main :: IO ()
    main = do
      let xs = execWriter $ forM_ [1..1000000::Int] $ \n -> tell [n]
      putStrLn . show $ sum xs
  9. Using CPS WriterT for efficient accumulation

    master

    The Control.Monad.Trans.Writer.CPS implementation is a more robust alternative that behaves similarly to a StateT with a restricted API. It is designed to run in constant space by ensuring the bind is tail-recursive and strict, and by continuously evaluating the mappend operation to prevent thunk accumulation.

    Usage Considerations:

    • Efficiency: It runs in constant space for most monads and monoids.
    • Complexity Warning: Because it continuously evaluates mappend, the time complexity can degrade to O(n^2) if the monoid's append operation is inefficient (for example, appending to the end of a list [a]). It is best used with monoids where left-associated mappend chains are efficient.
    -- Example of efficient constant-space usage with CPS WriterT
    import Control.Monad.Trans.Writer.CPS
    import Data.Foldable
    import Data.Monoid
    
    main :: IO ()
    main = do
      let Sum xs = execWriter $ forM_ [1..1000000::Int] $ \n -> tell $ Sum n
      putStrLn $ show xs
  10. Why use effectful instead of mtl/transformers

    master

    The effectful library is designed to solve several predictable and subtle bugs inherent in the standard mtl and transformers libraries:

    • Error Handling: Unlike ExceptT, effectful's Error effect is implemented using runtime exceptions. This allows for automatic stack traces and enables users to treat Error-specific errors and runtime exceptions uniformly.
    • State Management: In mtl, StateT can discard state updates when interacting with runtime exceptions or ExceptT errors. In effectful, State effects never lose updates and are independent of the effect stack order.
    • Writer Effects: effectful provides properly strict Writer effects, avoiding the complexity and multiple variants (Lazy, Strict, CPS) found in transformers.
    • Stack Composition: Instead of complex combined transformers like RWST, effectful encourages stacking individual effects, which is computationally cheap and avoids the 'spike trap' of RWST's combined issues.
  11. How effectful-plugin improves effect disambiguation

    master

    The effectful-plugin helps GHC resolve effect types in ambiguous contexts. Without the plugin, polymorphic functions like get or put in an Eff block may require explicit type applications if the surrounding context (like numeric literals or polymorphic operators) doesn't provide enough type information. With the plugin enabled, GHC can often infer the correct effect type automatically.

    Without the plugin (requires explicit type application):

    action :: (State Int :> es, State String :> es) => Eff es ()
    action = do
      x <- get @Int
      put (x + 1)

    With the plugin (automatic inference):

    action :: (State Int :> es, State String :> es) => Eff es ()
    action = do
      x <- get
      put (x + 1)