dep-t 0.4.4.0 → 0.4.5.0
raw patch · 6 files changed
+128/−82 lines, 6 filesdep ~doctestPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependency ranges changed: doctest
API changes (from Hackage documentation)
+ Control.Monad.Dep.Has: asCall :: forall env m. env -> forall r_ x. Has r_ m env => (r_ m -> x) -> x
- Control.Monad.Dep.Has: class Has r_ d e | e -> d
+ Control.Monad.Dep.Has: class Has r_ m env | env -> m
- Control.Monad.Dep.Has: dep :: (Has r_ d e, Dep r_, HasField (DefaultFieldName r_) e u, Coercible u (r_ d)) => e -> r_ d
+ Control.Monad.Dep.Has: dep :: (Has r_ m env, Dep r_, HasField (DefaultFieldName r_) env u, Coercible u (r_ m)) => env -> r_ m
Files
- CHANGELOG.md +4/−0
- README.md +18/−3
- dep-t.cabal +2/−2
- lib/Control/Monad/Dep/Class.hs +1/−0
- lib/Control/Monad/Dep/Has.hs +91/−65
- test/tests_has.hs +12/−12
CHANGELOG.md view
@@ -1,5 +1,9 @@ # Revision history for dep-t +## 0.4.5.0 + +* added "asCall" to Control.Monad.Dep.Has + ## 0.4.4.0 * added Control.Monad.Dep.Has, a generic "Has" typeclass which favors a style in which
README.md view
@@ -41,7 +41,7 @@ instance HasRepository IO EnvIO where repository = _repositoryIO -Record-of-functions-in-IO is a simple technique which works well in many +[Record-of-functions-in-IO](https://www.fpcomplete.com/blog/2017/06/readert-design-pattern/) is a simple technique which works well in many situations. There are even [specialized libraries](http://hackage.haskell.org/package/rio) that support it. @@ -264,11 +264,26 @@ "call stack", or to implement something like Logback's [Mapped Diagnostic Context](http://logback.qos.ch/manual/mdc.html). -- [RIO](http://hackage.haskell.org/package/rio) is a featureful ReaderT-like / - prelude replacement library which favors monomorphic environments. + So perhaps `DepT` will be overkill in a lot of cases, offering unneeded + flexibility. [This + gist](https://gist.github.com/danidiaz/358fdccdef51ad37bbec932631dcc4d2) + contains an example of `DepT`-less and `ReaderT`-less dependency injection. + It does use `Control.Monad.Dep.Has`, in combination with "open" and "closed" + versions of the environment record. Unlike in "Adventures..." the gist + doesn't use an extensible record for the environment but, to keep things + simple, a suitably parameterized conventional one. - Another exploration of dependency injection with `ReaderT`: [ReaderT-OpenProduct-Environment](https://github.com/keksnicoh/ReaderT-OpenProduct-Environment). + +- [The ReaderT design pattern](https://www.fpcomplete.com/blog/2017/06/readert-design-pattern/). + +> Your application code will, in general, live in ReaderT Env IO. Define it as type App = ReaderT Env IO if you wish, or use a newtype wrapper instead of ReaderT directly. + +> Optional: instead of directly using the App datatype, write your functions in terms of mtl-style typeclasses like MonadReader and MonadIO + +- [RIO](http://hackage.haskell.org/package/rio) is a featureful ReaderT-like / + prelude replacement library which favors monomorphic environments. - The [van Laarhoven Free Monad](http://r6.ca/blog/20140210T181244Z.html).
dep-t.cabal view
@@ -1,7 +1,7 @@ cabal-version: 3.0 name: dep-t -version: 0.4.4.0 +version: 0.4.5.0 synopsis: Reader-like monad transformer for dependency injection. description: Put all your functions in the environment record! Let all your functions read from the environment record! No favorites! @@ -81,5 +81,5 @@ build-depends: dep-t, rank2classes ^>= 1.4.1, - doctest ^>= 0.17, + doctest ^>= 0.18,
lib/Control/Monad/Dep/Class.hs view
@@ -84,6 +84,7 @@ -- >>> :set -XFunctionalDependencies -- >>> :set -XFlexibleContexts -- >>> :set -XDataKinds +-- >>> import Control.Monad.Reader -- >>> import Control.Monad.Dep -- >>> import Rank2 qualified -- >>> import Rank2.TH qualified
lib/Control/Monad/Dep/Has.hs view
@@ -11,97 +11,117 @@ {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ViewPatterns #-} --- | This module provides a generic \"Has\" class favoring a style in which the --- components of the environment come wrapped in records or newtypes, instead --- of being bare functions. +-- | This module provides a general-purpose 'Has' class favoring a style in +-- which the components of the environment, instead of being bare functions, +-- are themselves records or newtypes containing functions. -- +-- In this style, the functions that are \"invoked\" from the environment are +-- actually record field selectors. These selectors guide the 'Has' class to +-- find the correct records in the environment. +-- -- >>> :{ -- type Logger :: (Type -> Type) -> Type --- newtype Logger d = Logger {log :: String -> d ()} deriving Generic +-- newtype Logger d = Logger {log :: String -> d ()} -- instance Dep Logger where -- type DefaultFieldName Logger = "logger" +-- -- -- data Repository d = Repository -- { select :: String -> d [Int], -- insert :: [Int] -> d () --- } deriving Generic +-- } -- instance Dep Repository where -- type DefaultFieldName Repository = "repository" --- newtype Controller d = Controller {serve :: Int -> d String} deriving Generic +-- -- +-- newtype Controller d = Controller {serve :: Int -> d String} -- instance Dep Controller where -- type DefaultFieldName Controller = "controller" +-- -- -- type Env :: (Type -> Type) -> Type -- data Env m = Env -- { logger :: Logger m, -- repository :: Repository m, -- controller :: Controller m -- } --- instance Has Logger m (Env m) --- instance Has Repository m (Env m) --- instance Has Controller m (Env m) --- mkController :: forall d e m. MonadDep [Has Logger, Has Repository] d e m => Controller m +-- -- instance Has Logger m (Env m) +-- -- instance Has Repository m (Env m) +-- -- instance Has Controller m (Env m) +-- :} +-- +-- 'Has' can be used in combination with 'MonadDep', like this: +-- +-- >>> :{ +-- mkController :: MonadDep [Has Logger, Has Repository] d env m => Controller m -- mkController = +-- Controller \url -> +-- useEnv \(asCall -> call) -> do +-- call log "I'm going to insert in the db!" +-- call select "select * from ..." +-- call insert [1, 2, 3, 4] +-- return "view" +-- :} +-- +-- 'Has' can also be used independently of 'MonadReader' or 'MonadDep'. Here +-- for example the environment is passed as a plain function argument, and @m@ +-- doesn't have any constraint other than 'Monad': +-- +-- >>> :{ +-- mkController' :: (Monad m, Has Logger m env, Has Repository m env) => env -> Controller m +-- mkController' (asCall -> call) = -- Controller \url -> do --- e <- ask --- liftD $ log (dep e) "I'm going to insert in the db!" --- liftD $ select (dep e) "select * from ..." --- liftD $ insert (dep e) [1, 2, 3, 4] +-- call log "I'm going to insert in the db!" +-- call select "select * from ..." +-- call insert [1, 2, 3, 4] -- return "view" -- :} -- --- The @adviseRecord@ and @deceiveRecord@ functions from the companion package --- \"dep-t-advice\" can facilitate working with this style of components. -- module Control.Monad.Dep.Has ( - -- * A generic \"Has\" - Has (..), + -- * A general-purpose Has + Has (..) + -- * call helper + , asCall -- * Component defaults - Dep (..) + , Dep (..) +-- , useCall ) where import Data.Kind import GHC.Records import GHC.TypeLits import Data.Coerce - --- $setup --- --- >>> :set -XTypeApplications --- >>> :set -XMultiParamTypeClasses --- >>> :set -XImportQualifiedPost --- >>> :set -XTemplateHaskell --- >>> :set -XStandaloneKindSignatures --- >>> :set -XNamedFieldPuns --- >>> :set -XFunctionalDependencies --- >>> :set -XFlexibleContexts --- >>> :set -XDataKinds --- >>> :set -XRankNTypes --- >>> :set -XBlockArguments --- >>> :set -XFlexibleInstances --- >>> :set -XTypeFamilies --- >>> :set -XDeriveGeneric --- >>> import Control.Monad.Dep --- >>> import Rank2 qualified --- >>> import Rank2.TH qualified --- >>> import GHC.Generics (Generic) --- - +-- import Control.Monad.Reader +-- import Control.Monad.Dep.Class -- | A generic \"Has\" class. When partially applied to a parametrizable --- record-of-functions @r_@, produces a 2-place constraint that can be later --- used with "Control.Monad.Dep.Class". +-- record-of-functions @r_@, produces a 2-place constraint that can used on its +-- own, or with "Control.Monad.Dep.Class". type Has :: ((Type -> Type) -> Type) -> (Type -> Type) -> Type -> Constraint -class Has r_ d e | e -> d where - -- | Given an environment @e@, produce a record-of-functions parameterized by the environment's effect monad @d@. +class Has r_ m env | env -> m where + -- | Given an environment @e@, produce a record-of-functions parameterized by the environment's effect monad @m@. -- -- The hope is that using a selector function on the resulting record will - -- determine its type without the need for type annotations. + -- fix the record's type without the need for type annotations. -- - -- (This will likely not play well with RecordDotSyntax. See also <https://chrisdone.com/posts/import-aliases-field-names/ this trick>.) - dep :: e -> r_ d - default dep :: (Dep r_, HasField (DefaultFieldName r_) e u, Coercible u (r_ d)) => e -> r_ d - dep e = coerce . getField @(DefaultFieldName r_) $ e + -- (This will likely not play well with RecordDotSyntax. See also <https://chrisdone.com/posts/import-aliases-field-names/ this import alias trick for avoiding name collisions>.) + dep :: env -> r_ m + default dep :: (Dep r_, HasField (DefaultFieldName r_) env u, Coercible u (r_ m)) => env -> r_ m + dep env = coerce . getField @(DefaultFieldName r_) $ env +-- | Transforms an environment with suitable 'Has' instances into a \"helper\" +-- function that looks in the environment for the arguments of other functions. +-- Typically, the \"helped\" functions will be record field selectors. +-- +-- In practice, this means that you can write @call foo@ instead of @foo (dep +-- env)@. +-- +-- Using 'asCall' in a view pattern avoids having to name the +-- environment. +asCall :: forall env m . env -> forall r_ x. Has r_ m env => (r_ m -> x) -> x +asCall env = \f -> f (dep env) + -- | Parametrizable records-of-functions can be given an instance of this -- typeclass to specify the default field name 'Has' expects for the component -- in the environment record. @@ -114,19 +134,25 @@ -- k type and use it as the default preferred field name. type DefaultFieldName r_ :: Symbol --- -- Doesn't make much sense to have this, we already have Has! --- type Sub :: ((Type -> Type) -> Type) -> ((Type -> Type) -> Type) -> Constraint --- class Sub sub super --- type SubWrapper :: ((Type -> Type) -> Type) -> ((Type -> Type) -> Type) -> (Type -> Type) -> Type -> Type --- data SubWrapper sub super d e = SubWrapper e - --- type Nested :: ((Type -> Type) -> Type) -> ((Type -> Type) -> Type) -> (Type -> Type) -> Type -> Type --- data Nested sub super d e = Nested e --- --- instance (Has sub d e, Has super d (sub d)) => Has super d (Nested sub super d e) where --- dep (Nested e) = dep @super (dep @sub e) - --- Possible example --- instance Has ReadRef IO (Env IO) via (Nested Ref ReadRef IO (Env IO)) +-- $setup +-- +-- >>> :set -XTypeApplications +-- >>> :set -XMultiParamTypeClasses +-- >>> :set -XImportQualifiedPost +-- >>> :set -XTemplateHaskell +-- >>> :set -XStandaloneKindSignatures +-- >>> :set -XNamedFieldPuns +-- >>> :set -XFunctionalDependencies +-- >>> :set -XFlexibleContexts +-- >>> :set -XDataKinds +-- >>> :set -XBlockArguments +-- >>> :set -XFlexibleInstances +-- >>> :set -XTypeFamilies +-- >>> :set -XDeriveGeneric +-- >>> :set -XViewPatterns +-- >>> import Data.Kind +-- >>> import Control.Monad.Dep +-- >>> import GHC.Generics (Generic) +--
test/tests_has.hs view
@@ -76,13 +76,12 @@ mkController :: forall d e m. MonadDep [Has Logger, Has Repository] d e m => Controller m mkController = - Controller \url -> do - e <- ask - liftD $ log (dep e) "I'm going to insert in the db!" - -- liftD $ (dep e).log "I'm going to insert in the db!" -- Once RecordDotSyntax arrives... - liftD $ select (dep e) "select * from ..." - liftD $ insert (dep e) [1, 2, 3, 4] - return "view" + Controller \url -> + useEnv \(asCall -> call) -> do + call log "I'm going to insert in the db!" + call select "select * from ..." + call insert [1, 2, 3, 4] + return "view" -- also toss in this helper function -- useEnv :: forall d e m r. (LiftDep d m, MonadReader e m) => (e -> d r) -> m r @@ -95,9 +94,10 @@ mkController' = Controller \url -> useEnv \e -> do - log (dep e) "I'm going to insert in the db!" - select (dep e) "select * from ..." - insert (dep e) [5, 3, 43] + let (asCall -> call) = e + call log "I'm going to insert in the db!" + call select "select * from ..." + call insert [5, 3, 43] return "view" type EnvIO :: Type @@ -123,8 +123,8 @@ liftD $ log (dep e) "I'm going to select an entity" return [], insert = \entity -> do - e <- ask - liftD $ log (dep e) "I'm going to write the entity!" + (asCall -> call) <- ask + liftD $ call log "I'm going to write the entity!" tell ([], entity) }