2019-03-20 06:42:18 +03:00
|
|
|
# polysemy
|
2019-03-19 06:04:21 +03:00
|
|
|
|
2019-04-10 07:17:26 +03:00
|
|
|
[![Build Status](https://api.travis-ci.org/isovector/polysemy.svg?branch=master)](https://travis-ci.org/isovector/polysemy)
|
2019-04-10 07:18:18 +03:00
|
|
|
[![Hackage](https://img.shields.io/hackage/v/polysemy.svg?logo=haskell)](https://hackage.haskell.org/package/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
|
|
|
|
|
|
|
|
`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.
|
|
|
|
|
2019-04-10 23:40:56 +03:00
|
|
|
It's like `freer-simple` but more powerful and 35x faster.
|
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
|
|
|
|
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? Check out
|
|
|
|
[polysemy-plugin](https://github.com/isovector/polysemy/tree/master/polysemy-plugin),
|
2019-05-14 04:16:34 +03:00
|
|
|
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:
|
|
|
|
|
|
|
|
```haskell
|
|
|
|
{-# OPTIONS_GHC -fplugin=Polysemy.Plugin #-}
|
|
|
|
```
|
|
|
|
Or by adding `-fplugin=Polysemy.Plugin` to your package.yaml/.cabal file `ghc-options` section.
|
2019-04-28 06:54:33 +03:00
|
|
|
|
2019-04-10 23:11:36 +03:00
|
|
|
|
|
|
|
## 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 GHC<sup>[1](#fn1)</sup> can optimize
|
|
|
|
away the entire abstraction at compile time.
|
|
|
|
|
|
|
|
|
2019-04-10 23:27:25 +03:00
|
|
|
<sup><a name="fn1">1</a></sup>: Unfortunately this is not true in GHC 8.6.3, but
|
|
|
|
will be true as soon as [my patch](https://gitlab.haskell.org/ghc/ghc/merge_requests/668/) lands.
|
2019-04-10 23:11:36 +03:00
|
|
|
|
|
|
|
|
|
|
|
## Examples
|
|
|
|
|
2019-04-19 18:52:43 +03:00
|
|
|
Make sure you read the [Necessary Language
|
|
|
|
Extensions](https://github.com/isovector/polysemy#necessary-language-extensions)
|
|
|
|
before trying these yourself!
|
|
|
|
|
2019-04-12 20:44:17 +03:00
|
|
|
Teletype effect:
|
2019-04-10 23:11:36 +03:00
|
|
|
|
|
|
|
```haskell
|
2019-04-11 02:11:41 +03:00
|
|
|
{-# LANGUAGE TemplateHaskell #-}
|
2019-04-30 22:18:46 +03:00
|
|
|
{-# LANGUAGE LambdaCase, BlockArguments #-}
|
|
|
|
{-# LANGUAGE GADTs, FlexibleContexts, TypeOperators, DataKinds, PolyKinds #-}
|
2019-04-10 23:11:36 +03:00
|
|
|
|
|
|
|
import Polysemy
|
2019-04-12 20:44:17 +03:00
|
|
|
import Polysemy.Input
|
|
|
|
import Polysemy.Output
|
2019-04-10 23:11:36 +03:00
|
|
|
|
2019-04-12 20:44:17 +03:00
|
|
|
data Teletype m a where
|
|
|
|
ReadTTY :: Teletype m String
|
|
|
|
WriteTTY :: String -> Teletype m ()
|
2019-04-10 23:11:36 +03:00
|
|
|
|
2019-04-12 20:44:17 +03:00
|
|
|
makeSem ''Teletype
|
2019-04-10 23:11:36 +03:00
|
|
|
|
2019-04-12 20:44:17 +03:00
|
|
|
runTeletypeIO :: Member (Lift IO) r => Sem (Teletype ': r) a -> Sem r a
|
|
|
|
runTeletypeIO = interpret $ \case
|
2019-04-19 18:52:43 +03:00
|
|
|
ReadTTY -> sendM getLine
|
|
|
|
WriteTTY msg -> sendM $ putStrLn msg
|
2019-04-12 20:44:17 +03:00
|
|
|
|
2019-04-30 22:18:46 +03:00
|
|
|
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
|
2019-04-12 20:44:17 +03:00
|
|
|
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
|
2019-04-12 20:44:17 +03:00
|
|
|
|
|
|
|
|
|
|
|
-- Let's pretend
|
2019-04-30 22:18:46 +03:00
|
|
|
echoPure :: [String] -> Sem '[] ([String], ())
|
|
|
|
echoPure = flip runTeletypePure echo
|
2019-04-12 20:44:17 +03:00
|
|
|
|
2019-04-30 22:18:46 +03:00
|
|
|
pureOutput :: [String] -> [String]
|
|
|
|
pureOutput = fst . run . echoPure
|
2019-04-12 20:44:17 +03:00
|
|
|
|
|
|
|
-- Now let's do things
|
|
|
|
echoIO :: Sem '[Lift IO] ()
|
|
|
|
echoIO = runTeletypeIO echo
|
|
|
|
|
2019-04-30 22:18:46 +03:00
|
|
|
-- echo forever
|
2019-04-12 20:44:17 +03:00
|
|
|
main :: IO ()
|
2019-04-30 22:18:46 +03:00
|
|
|
main = runM echoIO
|
2019-04-10 23:11:36 +03:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Resource effect:
|
|
|
|
|
|
|
|
```haskell
|
|
|
|
{-# LANGUAGE TemplateHaskell #-}
|
2019-04-30 22:18:46 +03:00
|
|
|
{-# LANGUAGE LambdaCase, BlockArguments #-}
|
|
|
|
{-# LANGUAGE GADTs, FlexibleContexts, TypeOperators, DataKinds, PolyKinds, TypeApplications #-}
|
2019-04-10 23:11:36 +03:00
|
|
|
|
2019-04-12 20:44:17 +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-12 20:44:17 +03:00
|
|
|
|
2019-04-30 22:18:46 +03:00
|
|
|
-- Using Teletype effect from above
|
2019-04-12 20:44:17 +03:00
|
|
|
|
|
|
|
data CustomException = ThisException | ThatException deriving Show
|
|
|
|
|
|
|
|
program :: Members '[Resource, Teletype, Error CustomException] r => Sem r ()
|
2019-05-14 04:17:05 +03:00
|
|
|
program = catch @CustomException work $ \e -> writeTTY ("Caught " ++ show e)
|
2019-04-12 20:44:17 +03:00
|
|
|
where work = bracket (readTTY) (const $ writeTTY "exiting bracket") $ \input -> do
|
|
|
|
writeTTY "entering bracket"
|
2019-04-30 22:18:46 +03:00
|
|
|
case input of
|
|
|
|
"explode" -> throw ThisException
|
|
|
|
"weird stuff" -> writeTTY input >> throw ThatException
|
|
|
|
_ -> writeTTY input >> writeTTY "no exceptions"
|
2019-04-12 20:44:17 +03:00
|
|
|
|
2019-04-30 22:18:46 +03:00
|
|
|
main :: IO (Either CustomException ())
|
|
|
|
main = (runM .@ runResource .@@ runErrorInIO @CustomException) . runTeletypeIO $ 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
|
|
|
|
. Member (Lift IO) r
|
2019-04-19 19:13:11 +03:00
|
|
|
=> (∀ x. Sem r x -> IO x)
|
|
|
|
-> Sem (Resource ': r) a
|
|
|
|
-> Sem r a
|
2019-04-10 23:22:07 +03:00
|
|
|
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.
|
|
|
|
|
|
|
|
```yaml
|
|
|
|
ghc-options: -O2 -flate-specialise -fspecialise-aggressively
|
|
|
|
default-extensions:
|
|
|
|
- DataKinds
|
|
|
|
- FlexibleContexts
|
|
|
|
- GADTs
|
|
|
|
- LambdaCase
|
|
|
|
- PolyKinds
|
|
|
|
- RankNTypes
|
|
|
|
- ScopedTypeVariables
|
|
|
|
- TypeApplications
|
|
|
|
- TypeOperators
|
|
|
|
- TypeFamilies
|
|
|
|
```
|