polysemy

Higher-order, low-boilerplate, zero-cost free monads. https://github.com/isovector/polysemy#readme

Version on this page:0.3.0.1
Stackage Nightly 2019-06-17:0.4.0.0
Latest on Hackage:0.4.0.0

See all snapshots polysemy appears in

polysemy

Build Status Hackage Hackage

Dedication

The word ‘good’ has many meanings. For example, if a man were to shoot his grandmother at a range of five hundred yards, I should call him a good shot, but not necessarily a good man.

Gilbert K. Chesterton

Overview

polysemy is a library for writing high-power, low-boilerplate, zero-cost, domain specific languages. It allows you to separate your business logic from your implementation details. And in doing so, polysemy lets you turn your implementation code into reusable library code.

It’s like mtl but composes better, requires less boilerplate, and avoids the O(n^2) instances problem.

It’s like freer-simple but more powerful and 35x faster.

It’s like fused-effects but with an order of magnitude less boilerplate.

Additionally, unlike mtl, polysemy has no functional dependencies, so you can use multiple copies of the same effect. This alleviates the need for ~~ugly hacks~~band-aids like classy lenses, the ReaderT pattern and nicely solves the trouble with typed errors.

Concerned about type inference? Check out polysemy-plugin, which should perform just as well as mtl‘s! Add polysemy-plugin to your package.yaml or .cabal file’s dependencies section to use. Then turn it on with a pragma in your source-files:

{-# OPTIONS_GHC -fplugin=Polysemy.Plugin #-}

Or by adding -fplugin=Polysemy.Plugin to your package.yaml/.cabal file ghc-options section.

Features

  • Effects are higher-order, meaning it’s trivial to write bracket and local as first-class effects.
  • Effects are low-boilerplate, meaning you can create new effects in a single-digit number of lines. New interpreters are nothing but functions and pattern matching.
  • Effects are zero-cost, meaning that GHC1 can optimize away the entire abstraction at compile time.

1: Unfortunately this is not true in GHC 8.6.3, but will be true in GHC 8.10.1.

Examples

Make sure you read the Necessary Language Extensions before trying these yourself!

Teletype effect:

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE LambdaCase, BlockArguments #-}
{-# LANGUAGE GADTs, FlexibleContexts, TypeOperators, DataKinds, PolyKinds #-}

import Polysemy
import Polysemy.Input
import Polysemy.Output

data Teletype m a where
  ReadTTY  :: Teletype m String
  WriteTTY :: String -> Teletype m ()

makeSem ''Teletype

runTeletypeIO :: Member (Lift IO) r => Sem (Teletype ': r) a -> Sem r a
runTeletypeIO = interpret $ \case
  ReadTTY      -> sendM getLine
  WriteTTY msg -> sendM $ putStrLn msg

runTeletypePure :: [String] -> Sem (Teletype ': r) a -> Sem r ([String], a)
runTeletypePure i
  = runFoldMapOutput pure  -- For each WriteTTY in our program, consume an output by appending it to the list in a ([String], a)
  . runListInput i         -- Treat each element of our list of strings as a line of input
  . reinterpret2 \case     -- Reinterpret our effect in terms of Input and Output
      ReadTTY -> maybe "" id <$> input
      WriteTTY msg -> output msg


echo :: Member Teletype r => Sem r ()
echo = do
  i <- readTTY
  case i of
    "" -> pure ()
    _  -> writeTTY i >> echo


-- Let's pretend
echoPure :: [String] -> Sem '[] ([String], ())
echoPure = flip runTeletypePure echo

pureOutput :: [String] -> [String]
pureOutput = fst . run . echoPure

-- Now let's do things
echoIO :: Sem '[Lift IO] ()
echoIO = runTeletypeIO echo

-- echo forever
main :: IO ()
main = runM echoIO

Resource effect:

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE LambdaCase, BlockArguments #-}
{-# LANGUAGE GADTs, FlexibleContexts, TypeOperators, DataKinds, PolyKinds, TypeApplications #-}

import Polysemy
import Polysemy.Input
import Polysemy.Output
import Polysemy.Error
import Polysemy.Resource

-- Using Teletype effect from above

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 = (runM .@ runResource .@@ runErrorInIO @CustomException) . runTeletypeIO $ program

Easy.

Friendly Error Messages

Free monad libraries aren’t well known for their ease-of-use. But following in the shoes of freer-simple, polysemy takes a serious stance on providing helpful error messages.

For example, the library exposes both the interpret and interpretH combinators. If you use the wrong one, the library’s got your back:

runResource
    :: forall r a
     . Member (Lift IO) r
    => (∀ x. Sem r x -> IO x)
    -> Sem (Resource ': r) a
    -> Sem r a
runResource finish = interpret $ \case
  ...

makes the helpful suggestion:

    • 'Resource' is higher-order, but 'interpret' can help only
      with first-order effects.
      Fix:
        use 'interpretH' instead.
    • In the expression:
        interpret
          $ \case

Likewise it will give you tips on what to do if you forget a TypeApplication or forget to handle an effect.

Don’t like helpful errors? That’s OK too — just flip the error-messages flag and enjoy the raw, unadulterated fury of the typesystem.

Necessary Language Extensions

You’re going to want to stick all of this into your package.yaml file.

  ghc-options: -O2 -flate-specialise -fspecialise-aggressively
  default-extensions:
    - DataKinds
    - FlexibleContexts
    - GADTs
    - LambdaCase
    - PolyKinds
    - RankNTypes
    - ScopedTypeVariables
    - TypeApplications
    - TypeOperators
    - TypeFamilies

Changes

Changelog for polysemy

0.3.0.1 (2019-06-09)

  • Fixed a type error in the benchmark caused by deprecation of Semantic

0.3.0.0 (2019-06-01)

  • Removed all deprecated names
  • Moved Random effect to polysemy-zoo
  • makeSem can now be used to create term-level operators (thanks to @TheMatten)

0.2.2.0 (2019-05-30)

  • Added getInspectorT to the Tactical functions, which allows polysemy code to be run in external callbacks
  • A complete rewrite of Polysemy.Internal.TH.Effect (thanks to @TheMatten)
  • Fixed a bug in the TH generation of effects where the splices could contain usages of effects that were ambiguous

0.2.1.0 (2019-05-27)

  • Fixed a bug in the Alternative instance for Sem, where it would choose the last success instead of the first
  • Added MonadPlus and MonadFail instances for Sem

0.2.0.0 (2019-05-23)

  • Fixed a serious bug in interpretH and friends, where higher-order effects would always be run with the current interpreter.
  • Lower precedence of .@ and .@@ to 8, from 9
  • Users need no longer require inlineRecursiveCalls — the polysemy-plugin-0.2.0.0 will do it automatically when compiling with -O
  • Deprecated inlineRecursiveCalls; slated for removal in the next version

0.1.2.1 (2019-05-18)

  • Give explicit package bounds for dependencies
  • Haddock improvements
  • Remove Typeable machinery from Polysemy.Internal.Union (thanks to @googleson78)

0.1.2.0 (2019-04-26)

  • runInputAsReader, runTraceAsOutput and runOutputAsWriter have more generalized types
  • Added runStateInIO
  • Added runOutputAsTrace
  • Added Members (thanks to @TheMatten)

0.1.1.0 (2019-04-14)

  • Added runIO interpretation (thanks to @adamConnerSax)
  • Minor documentation fixes

0.1.0.0 (2019-04-11)

  • Initial release

Unreleased changes

comments powered byDisqus