polysemy/README.md

322 lines
12 KiB
Markdown
Raw Normal View History

2019-05-30 10:01:16 +03:00
<p align="center">
2020-07-22 10:04:01 +03:00
<img src="https://raw.githubusercontent.com/polysemy-research/polysemy/master/polysemy.png" alt="Polysemy" title="Polysemy">
2019-05-30 10:01:16 +03:00
</p>
<p>&nbsp;</p>
2019-03-20 06:42:18 +03:00
# polysemy
2019-03-19 06:04:21 +03:00
2019-06-25 00:05:14 +03:00
[![Build Status](https://api.travis-ci.org/polysemy-research/polysemy.svg?branch=master)](https://travis-ci.org/polysemy-research/polysemy)
2019-05-24 02:24:57 +03:00
[![Hackage](https://img.shields.io/hackage/v/polysemy.svg?logo=haskell&label=polysemy)](https://hackage.haskell.org/package/polysemy)
[![Hackage](https://img.shields.io/hackage/v/polysemy-plugin.svg?logo=haskell&label=polysemy-plugin)](https://hackage.haskell.org/package/polysemy-plugin)
[![Zulip chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://funprog.zulipchat.com/#narrow/stream/216942-Polysemy)
2019-04-10 07:12:34 +03:00
2019-04-10 23:11:36 +03:00
## 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
2020-07-22 10:04:01 +03:00
`polysemy` is a library for writing high-power, low-boilerplate domain specific
languages. It allows you to separate your business logic from your
implementation details. And in doing so, `polysemy` lets you turn your
2019-04-10 23:11:36 +03:00
implementation code into reusable library code.
It's like `mtl` but composes better, requires less boilerplate, and avoids the
O(n^2) instances problem.
2020-07-22 10:04:01 +03:00
It's like `freer-simple` but more powerful.
2019-04-10 23:11:36 +03:00
It's like `fused-effects` but with an order of magnitude less boilerplate.
2019-04-28 06:54:33 +03:00
Additionally, unlike `mtl`, `polysemy` has no functional dependencies, so you
can use multiple copies of the same effect. This alleviates the need for ~~ugly
2020-07-22 10:04:01 +03:00
hacks~~ band-aids like
[classy lenses](http://hackage.haskell.org/package/lens-4.17.1/docs/Control-Lens-TH.html#v:makeClassy),
the
[`ReaderT` pattern](https://www.fpcomplete.com/blog/2017/06/readert-design-pattern)
and nicely solves the
[trouble with typed errors](https://www.parsonsmatt.org/2018/11/03/trouble_with_typed_errors.html).
Concerned about type inference? `polysemy` comes with its companion
[`polysemy-plugin`](https://github.com/isovector/polysemy/tree/master/polysemy-plugin),
which helps it 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:
```haskell
{-# OPTIONS_GHC -fplugin=Polysemy.Plugin #-}
```
2019-04-28 06:54:33 +03:00
2020-07-22 10:04:01 +03:00
Or by adding `-fplugin=Polysemy.Plugin` to your `package.yaml`/`.cabal` file `ghc-options` section.
2019-04-10 23:11:36 +03:00
## Features
2020-07-22 10:04:01 +03:00
- *Effects are higher-order,* meaning it's trivial to write `bracket` and `local`
2019-04-10 23:11:36 +03:00
as first-class effects.
2020-07-22 10:04:01 +03:00
- *Effects are low-boilerplate,* meaning you can create new effects in a
2019-04-10 23:11:36 +03:00
single-digit number of lines. New interpreters are nothing but functions and
pattern matching.
2019-09-04 17:36:41 +03:00
2019-11-05 16:01:17 +03:00
## Tutorials and Resources
2020-07-22 10:04:01 +03:00
- Raghu Kaippully wrote a beginner friendly
[tutorial](https://haskell-explained.gitlab.io/blog/posts/2019/07/28/polysemy-is-cool-part-1/index.html).
- Paweł Szulc gave a [great talk](https://youtu.be/idU7GdlfP9Q?t=1394) on how
to start thinking about polysemy.
- Sandy Maguire, the author, gave a talk on some of the
[performance implementation](https://www.youtube.com/watch?v=-dHFOjcK6pA)
- He has also written
[some](http://reasonablypolymorphic.com/blog/freer-higher-order-effects/)
[blog posts](http://reasonablypolymorphic.com/blog/tactics/) on other
implementation details.
2019-04-10 23:11:36 +03:00
## Examples
2020-07-22 10:04:01 +03:00
Make sure you read the
[Necessary Language Extensions](https://github.com/polysemy-research/polysemy#necessary-language-extensions)
before trying these yourself!
Teletype effect:
2019-04-10 23:11:36 +03:00
```haskell
2020-07-22 10:04:01 +03:00
{-# LANGUAGE TemplateHaskell, LambdaCase, BlockArguments, GADTs
, FlexibleContexts, TypeOperators, DataKinds, PolyKinds #-}
2019-04-10 23:11:36 +03:00
import Polysemy
import Polysemy.Input
import Polysemy.Output
2019-04-10 23:11:36 +03:00
data Teletype m a where
ReadTTY :: Teletype m String
WriteTTY :: String -> Teletype m ()
2019-04-10 23:11:36 +03:00
makeSem ''Teletype
2019-04-10 23:11:36 +03:00
teletypeToIO :: Member (Embed IO) r => Sem (Teletype ': r) a -> Sem r a
2020-07-22 10:04:01 +03:00
teletypeToIO = interpret \case
ReadTTY -> embed getLine
WriteTTY msg -> embed $ putStrLn msg
2019-04-30 22:18:46 +03:00
runTeletypePure :: [String] -> Sem (Teletype ': r) a -> Sem r ([String], a)
runTeletypePure i
2020-07-22 10:04:01 +03:00
-- For each WriteTTY in our program, consume an output by appending it to the
-- list in a ([String], a)
= runOutputMonoid pure
-- Treat each element of our list of strings as a line of input
. runInputList i
-- Reinterpret our effect in terms of Input and Output
. reinterpret2 \case
ReadTTY -> maybe "" id <$> input
WriteTTY msg -> output msg
echo :: Member Teletype r => Sem r ()
2019-04-30 22:18:46 +03:00
echo = do
i <- readTTY
case i of
"" -> pure ()
_ -> writeTTY i >> echo
-- Let's pretend
2019-04-30 22:18:46 +03:00
echoPure :: [String] -> Sem '[] ([String], ())
echoPure = flip runTeletypePure echo
2019-04-30 22:18:46 +03:00
pureOutput :: [String] -> [String]
pureOutput = fst . run . echoPure
2019-04-30 22:18:46 +03:00
-- echo forever
main :: IO ()
main = runM . teletypeToIO $ echo
2019-04-10 23:11:36 +03:00
```
Resource effect:
```haskell
2020-07-22 10:04:01 +03:00
{-# LANGUAGE TemplateHaskell, LambdaCase, BlockArguments, GADTs
, FlexibleContexts, TypeOperators, DataKinds, PolyKinds
, TypeApplications #-}
2019-04-10 23:11:36 +03:00
import Polysemy
import Polysemy.Input
import Polysemy.Output
import Polysemy.Error
2019-04-30 22:18:46 +03:00
import Polysemy.Resource
2019-04-30 22:18:46 +03:00
-- Using Teletype effect from above
data CustomException = ThisException | ThatException deriving Show
program :: Members '[Resource, Teletype, Error CustomException] r => Sem r ()
2020-07-22 10:04:01 +03:00
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"
2019-04-30 22:18:46 +03:00
main :: IO (Either CustomException ())
2020-07-22 10:04:01 +03:00
main
= runFinal
. embedToFinal @IO
. resourceToIOFinal
. errorToIOFinal @CustomException
. teletypeToIO
$ program
2019-04-10 23:11:36 +03:00
```
Easy.
2019-03-19 06:04:21 +03:00
2019-04-10 23:22:07 +03:00
## 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:
```haskell
runResource
:: forall r a
2019-06-12 16:36:08 +03:00
. Sem (Resource ': r) a
-> Sem r a
2019-06-12 16:36:08 +03:00
runResource = interpret $ \case
2019-04-10 23:22:07 +03:00
...
```
makes the helpful suggestion:
2020-07-22 10:04:01 +03:00
```txt
• 'Resource' is higher-order, but 'interpret' can help only
with first-order effects.
Fix:
use 'interpretH' instead.
• In the expression:
interpret
$ \case
2019-04-10 23:22:07 +03:00
```
Likewise it will give you tips on what to do if you forget a `TypeApplication`
or forget to handle an effect.
2020-07-22 10:04:01 +03:00
Don't like helpful errors? That's OK too - just flip the `error-messages`
flag and enjoy the raw, unadulterated fury of the typesystem.
2019-04-10 23:22:07 +03:00
## Necessary Language Extensions
You're going to want to stick all of this into your `package.yaml` file.
```yaml
ghc-options: -O2 -flate-specialise -fspecialise-aggressively
default-extensions:
- DataKinds
- FlexibleContexts
- GADTs
- LambdaCase
- PolyKinds
- RankNTypes
- ScopedTypeVariables
- TypeApplications
- TypeOperators
- TypeFamilies
```
2020-07-22 10:04:01 +03:00
## *What about performance?* ([TL;DR](#tldr))
Previous versions of this `README` mentioned **the library being**
***zero-cost***, as in having no visible effect on performance. While this was
the original motivation and main factor in implementation of this library, it
turned out that
[**optimizations** we depend on](https://reasonablypolymorphic.com/blog/specialization/),
while showing amazing results in small benchmarks, **don't work in
[bigger, multi-module programs](https://github.com/ghc-proposals/ghc-proposals/pull/313#issuecomment-590143835)**,
what greatly limits their usefulness.
What's more interesting though is that
this **isn't a `polysemy`-specific** problem - basically **all popular effects
libraries** ended up being bitten by variation of this problem in one way or
another, resulting in
[visible drop in performance](https://github.com/lexi-lambda/ghc-proposals/blob/delimited-continuation-primops/proposals/0000-delimited-continuation-primops.md#putting-numbers-to-the-cost)
compared to equivalent code without use of effect systems.
*Why did nobody notice this?*
One factor may be that while GHC's optimizer is
very, very good in general in optimizing all sorts of abstraction, it's
relatively complex and hard to predict - authors of libraries may have not
deemed location of code relevant, even though it had big effect at the end.
The other is that maybe **it doesn't matter as much** as we like to tell
ourselves. Many of these effects
libraries are used in production and they're doing just fine, because maximum
performance usually matters in small, controlled areas of code, that often
don't use features of effect systems at all.
*What can we do about this?*
Luckily, the same person that uncovered this problems proposed a
[solution](https://github.com/lexi-lambda/ghc-proposals/blob/delimited-continuation-primops/proposals/0000-delimited-continuation-primops.md) -
set of primops that will allow interpretation of effects at runtime, with
minimal overhead. It's not *zero-cost* as we hoped for with `polysemy` at
first, but it should have negligible effect on performance in real life and
compared to current solutions, it should be much more predictable and even
resolve some problems with behaviour of
[specific effects](https://github.com/polysemy-research/polysemy/issues/246).
You can try out experimental library that uses proposed features
[here](https://github.com/hasura/eff).
When it comes to `polysemy`, once GHC proposal lands, we consider option of
switching to implementation based on it. This will probably require some
breaking changes, but should resolve performance issues and maybe even make
implementation of higher-order effects easier.
If you're interested in more details, see
Alexis King's
[talk about the problem](https://www.youtube.com/watch?v=0jI-AlWEwYI),
Sandy Maguire's
[followup about how it relates to `polysemy`](https://reasonablypolymorphic.com/blog/mea-culpa/) and
[GHC proposal](https://github.com/ghc-proposals/ghc-proposals/pull/313) that
adds features needed for new type of implementation.
### TL;DR
Basically all current effects libraries (including `polysemy` and
even `mtl`) got performance wrong - **but**, there's ongoing work on extending
GHC with features that will allow for creation of effects implementation with
stable and strong performance. It's what `polysemy` may choose at some point,
but it will probably require few breaking changes.
2019-11-05 23:21:38 +03:00
## Acknowledgements, citations, and related work
The following is a non-exhaustive list of people and works that have had a
significant impact, directly or indirectly, on `polysemy`s design and
implementation:
2020-07-22 10:04:01 +03:00
- Oleg Kiselyov, Amr Sabry, and Cameron Swords —
[Extensible Effects: An alternative to monad transfomers][oleg:exteff]
- Oleg Kiselyov and Hiromi Ishii —
[Freer Monads, More Extensible Effects][oleg:more]
- Nicolas Wu, Tom Schrijvers, and Ralf Hinze —
[Effect Handlers in Scope][wu:scope]
- Nicolas Wu and Tom Schrijvers —
[Fusion for Free: Efficient Algebraic Effect Handlers][schrijvers:fusion]
- Andy Gill and other contributors — [`mtl`][hackage:mtl]
- Rob Rix, Patrick Thomson, and other contributors —
[`fused-effects`][gh:fused-effects]
- Alexis King and other contributors — [`freer-simple`][gh:freer-simple]
2019-11-05 23:21:38 +03:00
[docs]: https://hasura.github.io/eff/Control-Effect.html
[gh:fused-effects]: https://github.com/fused-effects/fused-effects
[gh:freer-simple]: https://github.com/lexi-lambda/freer-simple
[hackage:mtl]: https://hackage.haskell.org/package/mtl
[oleg:exteff]: http://okmij.org/ftp/Haskell/extensible/exteff.pdf
[oleg:more]: http://okmij.org/ftp/Haskell/extensible/more.pdf
[schrijvers:fusion]: https://people.cs.kuleuven.be/~tom.schrijvers/Research/papers/mpc2015.pdf
[wu:scope]: https://www.cs.ox.ac.uk/people/nicolas.wu/papers/Scope.pdf