can-i-haz-0.2.0.1: Generic implementation of the Has and CoHas patterns

Stabilityexperimental
Safe HaskellSafe
LanguageHaskell2010

Control.Monad.Reader.Has

Description

This module defines a class Has intended to be used with the MonadReader class or Reader / ReaderT types.

The problem

Assume there are two types representing the MonadReader environments for different parts of an application:

data DbConfig = DbConfig { .. }
data WebConfig = WebConfig { .. }

as well as a single type containing both of those:

data AppEnv = AppEnv
  { dbConfig :: DbConfig
  , webConfig :: WebConfig
  }

What should be the MonadReader constraint of the DB module and web module respectively?

  1. It could be MonadReader AppEnv m for both, introducing unnecessary coupling.
  2. Or it could be MonadReader DbConfig m for the DB module and MonadReader WebConfig m for the web module respectively, but combining them becomes a pain.

Or, it could be MonadReader r m, Has DbConfig r for the DB module (and similarly for the web module), where some appropriately defined Has part record class allows projecting part out of some record. This approach keeps both modules decoupled, while allowing using them in the same monad stack.

The only downside is that now one has to define the Has class and write tediuos instances for the AppEnv type (and potentially other types in case of, for example, tests).

But why bother doing the work that the machine will happily do for you?

The solution

This module defines the generic Has class as well as hides all the boilerplate behind GHC.Generics, so all you have to do is to add the corresponding deriving-clause:

data AppEnv = AppEnv
  { dbConfig :: DbConfig
  , webConfig :: WebConfig
  } deriving (Generic, Has DbConfig, Has WebConfig)

and use ask extract instead of ask (but this is something you'd have to do anyway).

Note that this Generic-based implementation walks the types recursively, so it's totally possible to derive Has for some nested type, for example:

data DbConfig = DbConfig
  { connInfo :: ConnInfo
  , retriesPolicy :: RetriesPolicy
  }
data WebConfig = WebConfig { .. }

data AppEnv = AppEnv
  { dbConfig :: DbConfig
  , webConfig :: WebConfig
  } deriving (Generic, Has DbConfig, Has WebConfig, Has ConnInfo, Has RetriesPolicy)

Type safety

What should happen if record does not have any field of type part at all? Of course, this means that we cannot project part out of record, and no Has instance can be derived at all. Indeed, this library will refuse to generate an instance in this case.

On the other hand, what should happen if record contains multiple values of type part, perhaps on different levels of nesting? While technically we could make an arbitrary choice, like taking the first one in breadth-first or depth-first order, we instead decide that such a choice is inherently ambiguous, so this library will refuse to generate an instance in this case as well.

Synopsis

Documentation

class Has part record where Source #

The Has part record class is used for records of type record supporting projecting out a value of type part.

Minimal complete definition

Nothing

Methods

extract :: record -> part Source #

Extract a subvalue of type part from the record.

The default implementation searches for some value of the type part in record (potentially recursively) and returns that value. The default implementation typechecks if and only if there is a single subvalue of type part in record.

extract :: forall path. (Generic record, SuccessfulSearch part record path) => record -> part Source #

Extract a subvalue of type part from the record.

The default implementation searches for some value of the type part in record (potentially recursively) and returns that value. The default implementation typechecks if and only if there is a single subvalue of type part in record.

Instances
Has record record Source #

Each type allows projecting itself (and that is an id projection).

Instance details

Defined in Control.Monad.Reader.Has

Methods

extract :: record -> record Source #

SuccessfulSearch a (a0, a1) path => Has a (a0, a1) Source # 
Instance details

Defined in Control.Monad.Reader.Has

Methods

extract :: (a0, a1) -> a Source #

SuccessfulSearch a (a0, a1, a2) path => Has a (a0, a1, a2) Source # 
Instance details

Defined in Control.Monad.Reader.Has

Methods

extract :: (a0, a1, a2) -> a Source #

SuccessfulSearch a (a0, a1, a2, a3) path => Has a (a0, a1, a2, a3) Source # 
Instance details

Defined in Control.Monad.Reader.Has

Methods

extract :: (a0, a1, a2, a3) -> a Source #

SuccessfulSearch a (a0, a1, a2, a3, a4) path => Has a (a0, a1, a2, a3, a4) Source # 
Instance details

Defined in Control.Monad.Reader.Has

Methods

extract :: (a0, a1, a2, a3, a4) -> a Source #

SuccessfulSearch a (a0, a1, a2, a3, a4, a5) path => Has a (a0, a1, a2, a3, a4, a5) Source # 
Instance details

Defined in Control.Monad.Reader.Has

Methods

extract :: (a0, a1, a2, a3, a4, a5) -> a Source #

type SuccessfulSearch part record path = (Search part (Rep record) ~ Found path, GHas path part (Rep record)) Source #

Type alias representing that the search of part in record has been successful.

The path is used to guide the default generic implementation of Has.