capability-0.1.0.0: Extensional capabilities and deriving combinators

Safe HaskellSafe
LanguageHaskell2010

Capability

Description

A capability is a type class over a monad which specifies the effects that a function is allowed to perform. Capabilities differ from traditional monad transformer type classes in that they are completely independent of the way the monad is constructed. A state capability can for instance be implemented as a lens on a field in a larger state monad, or an error capability could provide for throwing only a subset of the errors of an error monad.

This library defines several standard, reusable capabilities that replace the mtl's monad-transformer type classes. Because capabilities are not tied to a particular implementation of the monad, they cannot be discharged by instance resolution. Instead this library provides combinators in the form of newtypes with instances, to be used with deriving-via. To learn about deriving via, watch Baldur Blondal's introductory video https://skillsmatter.com/skillscasts/10934-lightning-talk-stolen-instances-taste-just-fine.

By way of comparison, with the mtl you would write something like

foo :: (MonadReader E, MonadState S) => a -> m ()

You can use foo at type a -> ReaderT E (State S). But you can't use foo with the ReaderT pattern https://www.fpcomplete.com/blog/2017/06/readert-design-pattern. With this library, you would instead have:

foo :: (HasReader "conf" E, HasState "st" S) => a -> m ()

Where "conf" and "st" are the names (also referred to as tags) of the capabilities demanded by foo. Contrary to the mtl, capabilities are named, rather than disambiguated by the type of their implied state, or exception. This makes it easy to have multiple state capabilities.

To provide these capabilities, for instance with the ReaderT pattern, do as follows (for a longer form tutorial, check the README):

newtype MyM a = MyM (ReaderT (E, IORef s))
  deriving (Functor, Applicative, Monad)
  deriving (HasState "st" Int) via
    ReaderIORef (Rename 2 (Pos 2 ()
    (MonadReader (ReaderT (E, IORef s) IO))))
  deriving (HasReader "conf" Int) via
    (Rename 1 (Pos 1 ()
    (MonadReader (ReaderT (E, IORef s) IO))))

Then you can use foo at type MyM. Or any other type which can provide these capabilites.

Module structure

Each module introduces a capability type class (or several related type classes). Each class comes with a number of instances on newtypes (each newtype should be seen as a combinator to be used with deriving-via to provide the capability). Many newtypes come from the common Capability.Accessors module (re-exported by each of the other modules), which in particular contains a number of ways to address components of a data type using the generic-lens library.

Some of the capability modules have a “discouraged” companion (such as Capability.Writer.Discouraged). These modules contain deriving-via combinators which you can use if you absolutely must: they are correct, but inefficient, so we recommend that you do not.

Further considerations

The tags of capabilities can be of any kind, they are not restricted to symbols. When exporting functions demanding capabilities in libraries, it is recommended to use a type as follows:

data Conf

foo :: HasReader Conf C => m ()

This way, Conf can be qualified in case of a name conflict with another library.