can-i-haz 0.1.0.0 → 0.1.0.1
raw patch · 4 files changed
+152/−2 lines, 4 filesPVP: minor bump suggested
API additions: PVP suggests at least a minor version bump
API changes (from Hackage documentation)
+ Control.Monad.Reader.Has: type SuccessfulSearch part record path = (Search part (Rep record) ~ 'Found path, GHas path part (Rep record))
Files
- ChangeLog.md +5/−0
- README.md +61/−0
- can-i-haz.cabal +2/−2
- src/Control/Monad/Reader/Has.hs +84/−0
ChangeLog.md view
@@ -1,5 +1,10 @@ # Changelog for can-i-haz +## 0.1.0.1++* Added documentation.+* Export the SearchSuccessful type which might aid hand-writing instances.+ ## 0.1.0.0 Initial release.
README.md view
@@ -1,3 +1,64 @@ # can-i-haz +[![Build Status][travis-badge]][travis]+[![Hackage][hackage-badge]][hackage]+[![Stackage LTS][stackage-lts-badge]][stackage-lts]+[![Stackage Nightly][stackage-nightly-badge]][stackage-nightly]+ `Generic` implementation of the Has-pattern (mostly useful with `MonadReader`).++## Motivation++Assume there are two types representing the `MonadReader` environments+for different parts of an app:+```haskell+data DbConfig = DbConfig { .. }+data WebConfig = WebConfig { .. }+```+as well as a single type containing both of those:+```haskell+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,+where `Has` class allows projecting `DbConfig` out of some `r`,+and similarly for the web module!+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 tests).++## The solution++This library saves you from this unnecessary boilerplate!+The only thing you have to do is to append the `deriving`-clause:+```haskell+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).++## Documentation++Perhaps the best source is the [Haddock docs](http://hackage.haskell.org/package/can-i-haz/docs/Control-Monad-Reader-Has.html).++[travis]: <https://travis-ci.org/0xd34df00d/can-i-haz>+[travis-badge]: <https://travis-ci.org/0xd34df00d/can-i-haz.svg?branch=master>+[hackage]: <https://hackage.haskell.org/package/can-i-haz>+[hackage-badge]: <https://img.shields.io/hackage/v/can-i-haz.svg>+[stackage-lts-badge]: <http://stackage.org/package/can-i-haz/badge/lts>+[stackage-nightly-badge]: <http://stackage.org/package/can-i-haz/badge/nightly>+[stackage-lts]: <http://stackage.org/lts/package/can-i-haz>+[stackage-nightly]: <http://stackage.org/nightly/package/can-i-haz>
can-i-haz.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 859b8021274842401b027ff7b3ee5ec1352f7970b8feb144c40deffa5a44b026+-- hash: 1b0fd0317bcbd2ea753912693f9779a96e7743d0610d5c3c77e30e0206b23b70 name: can-i-haz-version: 0.1.0.0+version: 0.1.0.1 synopsis: Generic implementation of the Has pattern description: Please see the README on GitHub at <https://github.com/0xd34df00d/can-i-haz#readme> category: Control
src/Control/Monad/Reader/Has.hs view
@@ -1,9 +1,81 @@ {-# LANGUAGE TypeOperators, DataKinds, PolyKinds, TypeFamilies, ConstraintKinds #-} {-# LANGUAGE MultiParamTypeClasses, FlexibleInstances, UndecidableInstances, FlexibleContexts #-} {-# LANGUAGE ScopedTypeVariables, DefaultSignatures #-}+{-# LANGUAGE Safe #-}+{-# OPTIONS_HADDOCK show-extensions #-} +{-|+Description : Generic implementation of the Has pattern+Stability : experimental++This module defines a class 'Has' intended to be used with the 'Control.Monad.Reader.MonadReader' class+or 'Control.Monad.Reader.Reader' / 'Control.Monad.Reader.ReaderT' types.++= The problem++Assume there are two types representing the 'Control.Monad.Reader.MonadReader' environments+for different parts of an app:++@+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 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).++= 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 fail 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 fail to generate an instance in this case as well.++-}+ module Control.Monad.Reader.Has ( Has(..)+, SuccessfulSearch ) where import Data.Proxy@@ -40,13 +112,25 @@ instance GHas path part r => GHas ('R path) part (l :*: r) where gextract _ (_ :*: r) = gextract (Proxy :: Proxy path) r +-- | 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'. type SuccessfulSearch part record path = (Search part (Rep record) ~ 'Found path, GHas path part (Rep record)) +-- | The @Has part record@ class is used for records of type @record@ supporting+-- projecting out a value of type @part@. class Has part record where+ -- | 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 :: record -> part+ default extract :: forall path. (Generic record, SuccessfulSearch part record path) => record -> part extract = gextract (Proxy :: Proxy path) . from +-- | Each type allows projecting itself (and that is an 'id' projection). instance Has record record where extract = id