Polysemy

repository·master·Indexed 22 days ago

https://github.com/polysemy-research/polysemy

A high-performance, low-boilerplate effect system library for Haskell that allows developers to separate domain logic from side-effect implementations using an extensible effects approach. It provides tools for defining custom effects via GADTs, interpreting first-order and higher-order effects, and includes the polysemy-plugin to automate the disambiguation of effect constraints and improve type inference.

Tokens
2K
Snippets
6
Records
10
Agent score
28%

What's inside Polysemy

  1. Overview of Polysemy

    master
    Polysemy is a Haskell library for writing high-power, low-boilerplate domain-specific languages (DSLs) by separating business logic from implementation details. It is designed as an alternative to mtl, freer-simple, and fused-effects, offering better composition, less boilerplate, and the ability to use multiple copies of the same effect without functional dependency issues.
  2. How polysemy-plugin disambiguates effects

    master

    The polysemy-plugin helps the compiler resolve ambiguity when an effect is used but its specific type is not explicitly constrained in a way the standard polysemy typechecker can immediately verify.

    For example, in a program like:

    foo :: Member (State Int) r => Sem r ()
    foo = put 10

    Without the plugin, the compiler might fail because it cannot be certain if you intended to use a different State effect that also satisfies the required type class constraints. The plugin automates this disambiguation when the intent is clear.

  3. Enable polysemy-plugin via GHC options

    master

    To use the polysemy-plugin to disambiguate effect usage in your Polysemy programs, add the plugin to your package configuration using the -fplugin GHC option. This allows the typechecker to automatically resolve 'obvious' effect constraints that would otherwise cause ambiguity errors.

    ghc-options: -fplugin=Polysemy.Plugin
  4. Enable Polysemy type inference with Polysemy.Plugin

    master

    To ensure type inference performs as well as mtl, use the polysemy-plugin.

    1. Add polysemy-plugin to your package.yaml or .cabal file's dependencies section.
    2. Enable the plugin in your source files using a pragma, or globally in your build configuration.
    -- In your source file
    {-# OPTIONS_GHC -fplugin=Polysemy.Plugin #-}

    -- Or in package.yaml / .cabal -- ghc-options: -fplugin=Polysemy.Plugin

  5. Handle multiple ambiguous effects with type applications

    master

    The polysemy-plugin only disambiguates effects when there is exactly one relevant constraint in scope. If multiple effects of the same type (e.g., two different State effects) are present in the Members list, the plugin cannot decide which one to use and will not intervene.

    In such cases, you must manually resolve the ambiguity using a type application (e.g., @Int).

    Ambiguous case (Plugin will not help):

    bar :: Members '[ State Int, State Double ] r => Sem r ()
    bar = put 10

    Resolved case (Manual type application):

    bar :: Members '[ State Int, State Double ] r => Sem r ()
    bar = put @Int 10
  6. Build Polysemy using Nix

    master

    The project provides Nix configurations for development. You can build the library or the plugin using nix-build or the Flake interface.

    Using nix-build:

    nix-build -A polysemy
    nix-build -A polysemy-plugin

    Using Flakes:

    nix build
    nix build '.#polysemy-plugin'

    Running a development shell:

    nix-shell --pure
    nix-shell --pure --run 'cabal v2-haddock polysemy'
    nix-shell --pure --run ghcid
    
    # Flake version
    nix develop -i
    nix develop -i -c haskell-language-server-wrapper
  7. Configure required language extensions

    master

    To use Polysemy effectively, add the following extensions to your project configuration (e.g., package.yaml or .cabal):

      ghc-options: -O2 -flate-specialise -fspecialise-aggressively
      default-extensions:
        - DataKinds
        - FlexibleContexts
        - GADTs
        - LambdaCase
        - PolyKinds
        - RankNTypes
        - ScopedTypeVariables
        - TypeApplications
        - TypeOperators
        - TypeFamilies
  8. Troubleshoot effect interpretation errors

    master

    Polysemy provides helpful error messages when using the wrong combinator.

    If you attempt to use interpret on a higher-order effect (like Resource), the error message will explicitly state that interpret only works with first-order effects and suggest using interpretH instead.

    Example Error:

    • 'Resource' is higher-order, but 'interpret' can help only
      with first-order effects.
      Fix:
        use 'interpretH' instead.
  9. Use higher-order effects like Resource and Error

    master

    Polysemy supports higher-order effects such as Resource (for bracket) and Error. When using these, you must use the appropriate final interpreters (e.g., resourceToIOFinal, errorToIOFinal) to run the effect stack to IO.

    {-# LANGUAGE TemplateHaskell, LambdaCase, BlockArguments, GADTs
               , FlexibleContexts, TypeOperators, DataKinds, PolyKinds
               , TypeApplications #-}
    
    import Polysemy
    import Polysemy.Input
    import Polysemy.Output
    import Polysemy.Error
    import Polysemy.Resource
    
    -- Assuming Teletype is defined as in the previous example
    data CustomException = ThisException | ThatException deriving Show
    
    program :: Members '[Resource, Teletype, Error CustomException] r => Sem r ()
    program = catch @CustomException work \e -> writeTTY $ "Caught " ++ show e
     where
      work = bracket (readTTY) (const $ writeTTY "exiting bracket") \input -> do
        writeTTY "entering bracket"
        case input of
          "explode"     -> throw ThisException
          "weird stuff" -> writeTTY input *> throw ThatException
          _             -> writeTTY input *> writeTTY "no exceptions"
    
    main :: IO (Either CustomException ())
    main
      = runFinal
      . embedToFinal @IO
      . resourceToIOFinal
      . errorToIOFinal @CustomException
      . teletypeToIO
      $ program
  10. Define and interpret a custom effect

    master

    To create a new effect, define a GADT representing the operations and use makeSem to generate the effect functions. You can then write interpreters using interpret (for first-order effects) or interpretH (for higher-order effects like bracket or local).

    Example of a Teletype effect with a pure interpreter and an IO interpreter:

    {-# LANGUAGE TemplateHaskell, LambdaCase, BlockArguments, GADTs
               , FlexibleContexts, TypeOperators, DataKinds, PolyKinds, ScopedTypeVariables #-}
    
    import Polysemy
    import Polysemy.Input
    import Polysemy.Output
    
    data Teletype m a where
      ReadTTY  :: Teletype m String
      WriteTTY :: String -> Teletype m ()
    
    makeSem ''Teletype
    
    teletypeToIO :: Member (Embed IO) r => Sem (Teletype ': r) a -> Sem r a
    teletypeToIO = interpret \case
      ReadTTY      -> embed getLine
      WriteTTY msg -> embed $ putStrLn msg
    
    -- Usage in a program
    echo :: Member Teletype r => Sem r ()
    echo = do
      i <- readTTY
      case i of
        "" -> pure ()
        _  -> writeTTY i >> echo
    
    main :: IO ()
    main = runM . teletypeToIO $ echo