Extensional capabilities and deriving combinators.
Standard capability type classes for extensional effects and combinators to derive capability instances with little boilerplate.
capability: effects, extensionally
A capability is a type class that says explicitly which effects a function is allowed to use. The mtl
works like this too. But unlike the mtl
, this library decouples effects from their implementation. What this means in practice:
- You can implement large sets of capabilities using the efficient
ReaderT
pattern, rather than a slow monad transformer stack. - Capabilities compose well: e.g. it's easy to have multiple reader effects.
- You can use a writer effect without implementing it as a writer monad (which is known to leak space).
- You can reason about effects. For instance, if a monad provides a reader effect at type
IORef A
, it also provides a state effect at typeA
For more on these, you may want to read the announcement blog post.
This library is an alternative to the mtl
. It defines a set of standard, reusable capability type classes, such as the HasReader
and HasState
type classes, which provide the standard reader and state effects, respectively.
Where mtl
instances only need to be defined once and for all, capability-style programming has traditionally suffered from verbose boilerplate: rote instance definitions for every new implementation of the capability. Fortunately GHC 8.6 introduced the DerivingVia
language extension. We use it to remove the boilerplate, turning capability-style programming into an appealing alternative to mtl
-style programming. The generic-lens
library is used to access fields of structure in the style of the ReaderT
pattern.
An additional benefit of separating capabilities from their implementation is that they avoid a pitfall of the mtl
. In the mtl
, two different MonadState
are disambiguated by their types, which means that it is difficult to have two MonadState Int
in the same monad stack. Capability type classes are parameterized by a name (also known as a tag). This makes it possible to combine multiple versions of the same capability. For example,
twoStates :: (HasState "a" Int m, HasState "b" Int m) => m ()
Here, the tags "a"
and "b"
refer to different state spaces.
In summary, compared to the mtl
:
- capabilities represent what effects a function can use, rather than how the monad is constructed;
- capabilities are named, rather than disambiguated by type;
- capabilites are discharged with deriving-via combinators and
generic-lens
, rather than with instance resolution.
An example usage looks like this:
testParity :: (HasReader "foo" Int m, HasState "bar" Bool m) => m ()
testParity = do
num <- ask @"foo"
put @"bar" (even num)
data Ctx = Ctx { foo :: Int, bar :: IORef Bool }
deriving Generic
newtype M a = M { runM :: Ctx -> IO a }
deriving (Functor, Applicative, Monad) via ReaderT Ctx IO
-- Use DerivingVia to derive a HasReader instance.
deriving (HasReader "foo" Int, HasSource "foo" Int) via
-- Pick the field foo from the Ctx record in the ReaderT environment.
Field "foo" "ctx" (MonadReader (ReaderT Ctx IO))
-- Use DerivingVia to derive a HasState instance.
deriving (HasState "bar" Bool, HasSource "bar" Bool, HasSink "bar" Bool) via
-- Convert a reader of IORef to a state capability.
ReaderIORef (Field "bar" "ctx" (MonadReader (ReaderT Ctx IO)))
example :: IO ()
example = do
rEven <- newIORef False
runM testParity (Ctx 2 rEven)
readIORef rEven >>= print
runM testParity (Ctx 3 rEven)
readIORef rEven >>= print
For more complex examples, see the Examples section and the examples
subtree.
API documentation can be found on Hackage.
Examples
An example is provided in WordCount
. Execute the following commands to try it out:
$ nix-shell --pure --run "cabal configure --enable-tests"
$ nix-shell --pure --run "cabal repl examples"
ghci> :set -XOverloadedStrings
ghci> wordAndLetterCount "ab ba"
Letters
'a': 2
'b': 2
Words
"ab": 1
"ba": 1
To execute all examples and see if they produce the expected results run
$ nix-shell --pure --run "cabal test examples --show-details=streaming --test-option=--color"
Build instructions
Nix Shell
A development environment with all dependencies in scope is defined in shell.nix
.
Build
The build instructions assume that you have Nix installed. Execute the following command to build the library.
$ nix-shell --pure --run "cabal configure"
$ nix-shell --pure --run "cabal build"