dep-t 0.6.4.0 → 0.6.5.0
raw patch · 10 files changed
+615/−261 lines, 10 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Dep.Constructor: _accumConstructor :: forall accum deps component. (deps -> (accum, component)) -> AccumConstructor accum deps component
+ Dep.Constructor: _accumConstructor_ :: forall accum deps component. Monoid accum => (deps -> component) -> AccumConstructor accum deps component
+ Dep.Constructor: accumConstructor :: forall accum deps component. (accum -> deps -> (accum, component)) -> AccumConstructor accum deps component
+ Dep.Constructor: accumConstructor_ :: forall accum deps component. Monoid accum => (accum -> deps -> component) -> AccumConstructor accum deps component
+ Dep.Constructor: arr :: Arrow a => (b -> c) -> a b c
+ Dep.Constructor: constructor :: forall deps component. (deps -> component) -> Constructor deps component
+ Dep.Constructor: data AccumConstructor (accum :: Type) (deps :: Type) component
+ Dep.Constructor: data Constructor (deps :: Type) component
+ Dep.Constructor: fixEnv :: (Phased deps_, Typeable deps_, Typeable m) => deps_ (Constructor (deps_ Identity m)) m -> deps_ Identity m
+ Dep.Constructor: fixEnvAccum :: (Phased deps_, Typeable deps_, Typeable m, Monoid accum, Typeable accum) => deps_ (AccumConstructor accum (deps_ Identity m)) m -> (accum, deps_ Identity m)
+ Dep.Constructor: instance Control.Arrow.Arrow Dep.Constructor.Constructor
+ Dep.Constructor: instance Control.Category.Category Dep.Constructor.Constructor
+ Dep.Constructor: instance GHC.Base.Applicative (Dep.Constructor.Constructor deps)
+ Dep.Constructor: instance GHC.Base.Functor (Dep.Constructor.AccumConstructor accum deps)
+ Dep.Constructor: instance GHC.Base.Functor (Dep.Constructor.Constructor deps)
+ Dep.Constructor: instance GHC.Base.Monoid accum => Control.Arrow.Arrow (Dep.Constructor.AccumConstructor accum)
+ Dep.Constructor: instance GHC.Base.Monoid accum => Control.Category.Category (Dep.Constructor.AccumConstructor accum)
+ Dep.Constructor: instance GHC.Base.Monoid accum => GHC.Base.Applicative (Dep.Constructor.AccumConstructor accum deps)
+ Dep.Constructor: lmapAccumConstructor :: forall accum deps deps' component. Typeable component => (TypeRep -> deps -> deps') -> AccumConstructor accum deps' component -> AccumConstructor accum deps component
+ Dep.Constructor: lmapConstructor :: forall deps deps' component. Typeable component => (TypeRep -> deps -> deps') -> Constructor deps' component -> Constructor deps component
+ Dep.Env: liftAH :: forall deps_ phases phases' m. (Phased deps_, Typeable phases, Typeable phases', Typeable m) => (forall x. Typeable x => phases x -> phases' x) -> deps_ phases m -> deps_ phases' m
Files
- CHANGELOG.md +16/−0
- README.md +45/−243
- dep-t.cabal +2/−1
- lib/Control/Monad/Dep.hs +217/−7
- lib/Control/Monad/Dep/Class.hs +2/−1
- lib/Dep/Constructor.hs +302/−0
- lib/Dep/Env.hs +27/−8
- lib/Dep/Has.hs +1/−0
- test/doctests.hs +1/−0
- test/tests_env.hs +2/−1
CHANGELOG.md view
@@ -1,5 +1,21 @@ # Revision history for dep-t +## 0.6.5++* Deprecated `Constructor`, `fixEnv` and the `Accum-` counterparts from `Dep.Env`.+ + Created new versions in `Dep.Constructor`. The new versions are newtypes, not+ type aliases.++ Users trying to migrate should hide the old `Dep.Env` versions when importing+ the module.++ > import Dep.Env hiding (AccumConstructor, Constructor, accumConstructor, constructor, fixEnv, fixEnvAccum)++* Deprecated the `Dep` type family from `Dep.Has`.++* Shifted `DepT`-specific parts of the readme to `Control.Monad.Dep`.+ ## 0.6.4 * Added AccumConstructor, a generalization of Constructor that threads a
README.md view
@@ -1,259 +1,61 @@ # dep-t -This package provides various helpers for the "record-of-functions" style of structuring Haskell applications. The guiding idea is that record-of-functions is a form of dependency injection, and the that the environment which contains the functions is akin to an [`ApplicationContext`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/ApplicationContext.html) in object-oriented frameworks like [Java Spring](https://docs.spring.io/spring-framework/docs/current/reference/html/).--If every dependency knew about the concrete environment, that would increase coupling. The solution is to use `Has`-style classes so that each dependency knows only about those parts of the environment which it needs to function, and nothing more. Those `Has`-style classes can be tailor-made, but the package also provides a generic one. --*Very* loosely speaking, `Has`-style constraints correspond to injected member variables in object-oriented frameworks.--[](https://postimg.cc/V5bspcJB)--- __Dep.Has__ contains a generic `Has` typeclass for locating dependencies in an environment. It can be useful independently of `ReaderT`, `DepT` or any monad transformer.-- __Dep.Env__ complements __Dep.Has__, adding helpers for building environments of records.-- __Dep.Tagged__ is a helper for disambiguating dependencies in __Dep.Env__ environments.-- __Control.Monad.Dep__ contains the `DepT` monad transformer, a variant of `ReaderT`.-- __Control.Monad.Dep.Class__ is an extension of `MonadReader`, useful to program against both `ReaderT` and `DepT`.--## The DepT transformer--`DepT` is a-[ReaderT](http://hackage.haskell.org/package/mtl-2.2.2/docs/Control-Monad-Reader.html)-like-monad transformer for dependency injection.--The difference with `ReaderT` is that `DepT` takes an enviroment whose type is-parameterized by `DepT` itself.--### Rationale--To perform dependency injection in Haskell, a common solution is to build a-record of functions and pass it to the program logic using some variant of-[`ReaderT`](http://hackage.haskell.org/package/mtl-2.2.2/docs/Control-Monad-Reader.html).--To avoid becoming tied to a concrete reader environment, let's define some-auxiliary typeclasses that extract functions from a generic environment:-- type HasLogger :: (Type -> Type) -> Type -> Constraint- class HasLogger d e | e -> d where- logger :: e -> String -> d ()-- type HasRepository :: (Type -> Type) -> Type -> Constraint- class HasRepository d e | e -> d where- repository :: e -> Int -> d ()--We see that the type `e` of the environment determines the monad `d` on which-the effects take place.--Here's a monomorphic environment record with functions that have effects in `IO`:-- type EnvIO :: Type- data EnvIO = EnvIO- { _loggerIO :: String -> IO (),- _repositoryIO :: Int -> IO ()- }-- instance HasLogger IO EnvIO where- logger = _loggerIO-- instance HasRepository IO EnvIO where- repository = _repositoryIO--[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.--Here's a function which can get its dependencies from the monomorphic-environment:-- mkControllerIO :: (HasLogger IO e, HasRepository IO e) => Int -> ReaderT e IO String- mkControllerIO x = do- e <- ask- liftIO $ logger e "I'm going to insert in the db!"- liftIO $ repository e x- return "view"--That's all and well, but there are two issues that bug me:--- We might want to write code that is innocent of `IO` and polymorphic over the- monad, to ensure that the program logic can't do some unexpected missile- launch, or to allow testing our app in a "pure" way. --- What if the repository function needs access to the logger, too? The- repository lives in the environment record, but isn't aware of it. That means- it can't use the `HasLogger` typeclass for easy and convenient dependency- injection. Why privilege the controller in such a way?-- In a sufficiently complex app, the diverse functions that comprise it will be- organized in a big- [DAG](https://en.wikipedia.org/wiki/Directed_acyclic_graph) of dependencies.- And it would be nice if all the functions taking part in dependency injection- were treated uniformly; if all of them had access to (some view of) the- environment record.--To tackle these issues, we begin by giving the controller a more general signature:-- mkControllerIO :: (HasLogger IO e, HasRepository IO e, MonadIO m, MonadReader e m) => Int -> m String--Now the function can work in other reader-like monads besides `ReaderT`.--Let's go one step further, and abstract away the `IO`, so that functions in the-record can have effects in other monads:-- mkController :: (HasLogger d e, HasRepository d e, LiftDep d m, MonadReader e m) => Int -> m String- mkController x = do- e <- ask- liftD $ logger e "I'm going to insert in the db!"- liftD $ repository e x- return "view"--Now both the signature and the implementation have changed:--- There's a new type variable `d`, the monad in which functions taken from the- environment `e` have their effects.--- `MonadIO` has been replaced by `LiftDep` from `Control.Monad.Dep.Class`, a- constraint that says we can lift `d` effects into `m` (though it could still- make sense to require `MonadIO m` for effects not originating in the- environment).--- Uses of `liftIO` have been replaced by `liftD`.--If all those constraints prove annoying to write, there's a convenient shorthand using the `MonadDep` type family:-- mkController :: MonadDep [HasLogger, HasRepository] d e m => Int -> m String--The new, more polymorphic `mkController` function can replace the original `mkControllerIO`:-- mkControllerIO' :: (HasLogger IO e, HasRepository IO e) => Int -> ReaderT e IO String- mkControllerIO' = mkController--Now let's focus on the environment record. We'll parameterize its type by a-monad: -- type Env :: (Type -> Type) -> Type- data Env m = Env- { _logger :: String -> m (),- _repository :: Int -> m (),- _controller :: Int -> m String- }-- instance HasLogger m (Env m) where- logger = _logger-- instance HasRepository m (Env m) where- repository = _repository--Notice that the controller function is now part of the environment. No-favorites here!--The following implementation of the logger function has no dependencies besides-`MonadIO`:-- mkStdoutLogger :: MonadIO m => String -> m ()- mkStdoutLogger msg = liftIO (putStrLn msg)--But look at this implementation of the repository function. It gets hold of the-logger through `HasLogger`, just as the controller did:-- mkStdoutRepository :: (MonadDep '[HasLogger] d e m, MonadIO m) => Int -> m ()- mkStdoutRepository entity = do- e <- ask- liftD $ logger e "I'm going to write the entity!"- liftIO $ print entity--It's about time we choose a concrete monad and assemble an environment record:-- envIO :: Env (DepT Env IO)- envIO =- let _logger = mkStdoutLogger- _repository = mkStdoutRepository- _controller = mkController- in Env {_logger, _repository, _controller}--Not very complicated, except... what is that weird `DepT Env IO` doing there in-the signature? --Well, that's the whole reason this library exists. For dependency injection to-work for *all* functions, `Env` needs to be parameterized with a monad that-provides that same `Env` environment. And trying to use a `ReaderT (Env-something) IO` to parameterize `Env` won't fly; you'll get weird "infinite-type" kind of errors. So I created the `DepT` newtype over `ReaderT` to mollify-the compiler.--`DepT` has `MonadReader` and `LiftDep` instances, so the effects of-`mkController` can take place on it.--### So how do we invoke the controller now?--I suggest something like-- runDepT (do e <- ask; _controller e 7) envIO --or -- (do e <- ask; _controller e 7) `runDepT` envIO --The companion package-[dep-t-advice](http://hackage.haskell.org/package/dep-t-advice) has some more-functions for running `DepT` computations.--### How to avoid using "ask" and "liftD" before invoking a dependency?--One possible workaround (at the cost of more boilerplate) is to define helper-functions like: -- loggerD :: MonadDep '[HasLogger] d e m => String -> m ()- loggerD msg = asks logger >>= \f -> liftD $ f msg--Which you can invoke like this:-- usesLoggerD :: MonadDep [HasLogger, HasRepository] d e m => Int -> m String- usesLoggerD i = do- loggerD "I'm calling the logger!"- return "foo"--Though perhaps this isn't worth the hassle.--### How to use "pure fakes" during testing?--The [test suite](./test/tests.hs) has an example of using a `Writer` monad for-collecting the outputs of functions working as ["test-doubles"](https://martinfowler.com/bliki/TestDouble.html).+This package provides various helpers for the "record-of-functions" style of+structuring Haskell applications. -### How to make a function "see" a different evironment from the one seen by its dependencies?+A record that groups related functions is considered a component. Hypothetical example: -Sometimes we want a function in the environment to see a slightly different-record from the record seen by the other functions, and in particular from the-record seen by its own dependencies. +```+data Repository m = Repository+ { findById :: ResourceId -> m Resource,+ save :: Resource -> m ()+ } +``` -For example, the function might have a `HasLogger` constraint but we don't want-it to use the default `HasLogger` instance of the environment.+The record type is the component's "interface". A component's "implementation" is+defined by a constructor function that returns a value of the record type. -The companion package-[dep-t-advice](http://hackage.haskell.org/package/dep-t-advice) provides a-`deceive` function that allows for this.+When starting up, applications build a dependency injection environment+which contains all the required components. And components read their *own* dependencies+from the DI environment. The DI environment is akin to an+[`ApplicationContext`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/ApplicationContext.html)+in object-oriented frameworks like [Java+Spring](https://docs.spring.io/spring-framework/docs/current/reference/html/). -### How to add AOP-ish "aspects" to functions in an environment?+If components knew about the *concrete* DI environment, that would increase+coupling. Everything would depend on everything else. To avoid that, we resort+to `Has`-style typeclasses so that each constructor function knows only about the+parts of the environment that it needs, and nothing more. Those `Has`-style classes can+be tailor-made, but this package also provides a generic one. -The companion package-[dep-t-advice](http://hackage.haskell.org/package/dep-t-advice) provides a-general method of extending the behaviour of `DepT`-effectful functions, in a-way reminiscent of aspect-oriented programming.+Hypothetical example of constructor function: -### What if I don't want to use DepT, or any other monad transformer for that matter?+```+makeRepository :: (Has Logger m deps, Has SomeOtherDep m deps) => deps -> Repository m+``` -Check out the function `fixEnv` in module `Dep.Env`, which-provides a transformer-less way to perform dependency injection, based on-knot-tying.+*Very* loosely speaking, `Has`-style constraints correspond to [injected+constructor arguments](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-constructor-injection) in object-oriented DI frameworks. -That method requires an environment parameterized by _two_ type constructors:-one that wraps each field, and another that works as the effect monad for the-components.+## Module structure -### DepT caveats+```mermaid+ graph TD;+ Dep.Env-->Dep.Has;+ Dep.Constructor-->Dep.Env;+ Dep.Tagged;+ Control.Monad.Dep.Class-->Control.Monad.Reader;+ Control.Monad.Dep-->Control.Monad.Reader;+ Control.Monad.Dep-->Control.Monad.Dep.Class;+``` -The structure of the `DepT` type might be prone to trigger a [known infelicity-of the GHC-simplifier](https://twitter.com/DiazCarrete/status/1350116413445439493).+- __Dep.Has__ provides a generic `Has` typeclass for locating dependencies in an+environment. Usually, component implementations import this module.+- __Dep.Env__ complements __Dep.Has__ with helpers for building dependency injection environments. Usually, only the [composition root](https://stackoverflow.com/questions/6277771/what-is-a-composition-root-in-the-context-of-dependency-injection) of the application imports this module.+- __Dep.Tagged__ is a helper for disambiguating dependencies in __Dep.Env__ environments.+- __Dep.Constructor__ enables fixpoint-based dependency injection in __Dep.Env__ environments.+- __Control.Monad.Dep__ provides the `DepT` monad transformer, a variant of `ReaderT`. You either want to use this or __Dep.Constructor__ in your composition root, but not both.+- __Control.Monad.Dep.Class__ is an extension of `MonadReader`, useful to program against both `ReaderT` and `DepT`. ## Links
dep-t.cabal view
@@ -1,7 +1,7 @@ cabal-version: 3.0 name: dep-t-version: 0.6.4.0+version: 0.6.5.0 synopsis: Dependency injection for records-of-functions. description: Put all your functions in the environment record! Let all your functions read from the environment record! No favorites!@@ -28,6 +28,7 @@ import: common exposed-modules: Dep.Has Dep.Env+ Dep.Constructor Dep.Tagged Control.Monad.Dep Control.Monad.Dep.Class
lib/Control/Monad/Dep.hs view
@@ -11,15 +11,18 @@ {-# LANGUAGE KindSignatures #-} -- |--- This module provides 'DepT', a monad transformer similar to 'ReaderT'.------ The difference is that the environment of 'DepT' must be parameterized by--- @DepT@'s own monad stack.+-- This module provides 'DepT', a monad transformer similar to 'ReaderT'. ----- There's a function 'withDepT' which is analogous to 'withReaderT'.--- There's no analogue of 'mapReaderT' however. +-- The difference with 'ReaderT' is that 'DepT' takes an enviroment whose type is+-- parameterized by 'DepT' itself. module Control.Monad.Dep ( + -- * Motivation + -- $motivation++ -- ** Caveats+ -- $caveats+ -- * The DepT transformer DepT (DepT), runDepT,@@ -55,6 +58,211 @@ import Data.Coerce import Data.Functor.Constant +-- $motivation+--+-- Dependency injection.+--+-- To perform dependency injection in Haskell, a common solution is to build a+-- record of functions and pass it to the program logic using some variant of+-- 'ReaderT'.+-- +-- To avoid becoming tied to a concrete reader environment, let's define some+-- auxiliary typeclasses that extract functions from a generic environment:+--+-- @+-- type HasLogger :: (Type -> Type) -> Type -> Constraint+-- class HasLogger d e | e -> d where+-- logger :: e -> String -> d ()+-- +-- type HasRepository :: (Type -> Type) -> Type -> Constraint+-- class HasRepository d e | e -> d where+-- repository :: e -> Int -> d ()+-- @+-- +-- We see that the type `e` of the environment determines the monad `d` on which+-- the effects take place.+-- +-- Here's a monomorphic environment record with functions that have effects in `IO`:+-- +-- @+-- type EnvIO :: Type+-- data EnvIO = EnvIO+-- { _loggerIO :: String -> IO (),+-- _repositoryIO :: Int -> IO ()+-- }+-- +-- instance HasLogger IO EnvIO where+-- logger = _loggerIO+-- +-- instance HasRepository IO EnvIO where+-- repository = _repositoryIO+-- @+-- +-- [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.+-- +-- Here's a function which can get its dependencies from the monomorphic+-- environment:+-- +-- @+-- mkControllerIO :: (HasLogger IO e, HasRepository IO e) => Int -> ReaderT e IO String+-- mkControllerIO x = do+-- e <- ask+-- liftIO $ logger e "I'm going to insert in the db!"+-- liftIO $ repository e x+-- return "view"+-- @+-- +-- That's all and well, but there are two issues that bug me:+-- +-- - We might want to write code that is innocent of `IO` and polymorphic over the+-- monad, to ensure that the program logic can't do some unexpected missile+-- launch, or to allow testing our app in a "pure" way. +-- +-- - What if the repository function needs access to the logger, too? The+-- repository lives in the environment record, but isn't aware of it. That means+-- it can't use the `HasLogger` typeclass for easy and convenient dependency+-- injection. Why privilege the controller in such a way?+-- +-- In a sufficiently complex app, the diverse functions that comprise it will be+-- organized in a big+-- [DAG](https://en.wikipedia.org/wiki/Directed_acyclic_graph) of dependencies.+-- And it would be nice if all the functions taking part in dependency injection+-- were treated uniformly; if all of them had access to (some view of) the+-- environment record.+-- +-- To tackle these issues, we begin by giving the controller a more general signature:+-- +-- @+-- mkControllerIO :: (HasLogger IO e, HasRepository IO e, MonadIO m, MonadReader e m) => Int -> m String+-- @+-- +-- Now the function can work in other reader-like monads besides 'ReaderT'.+-- +-- Let's go one step further, and abstract away the `IO`, so that functions in the+-- record can have effects in other monads:+-- +-- @+-- mkController :: (HasLogger d e, HasRepository d e, LiftDep d m, MonadReader e m) => Int -> m String+-- mkController x = do+-- e <- ask+-- liftD $ logger e "I'm going to insert in the db!"+-- liftD $ repository e x+-- return "view"+-- @+-- +-- Now both the signature and the implementation have changed:+-- +-- - There's a new type variable `d`, the monad in which functions taken from the+-- environment `e` have their effects.+-- +-- - `MonadIO` has been replaced by `LiftDep` from `Control.Monad.Dep.Class`, a+-- constraint that says we can lift `d` effects into `m` (though it could still+-- make sense to require `MonadIO m` for effects not originating in the+-- environment).+-- +-- - Uses of `liftIO` have been replaced by `liftD`.+-- +-- If all those constraints prove annoying to write, there's a convenient shorthand using the `MonadDep` type family:+-- +-- @+-- MonadDep [HasLogger, HasRepository] d e m => Int -> m String+-- @+-- +-- The new, more polymorphic `mkController` function can replace the original `mkControllerIO`:+-- +-- @+-- mkControllerIO' :: (HasLogger IO e, HasRepository IO e) => Int -> ReaderT e IO String+-- mkControllerIO' = mkController+-- @+-- +-- Now let's focus on the environment record. We'll parameterize its type by a+-- monad: +-- +-- @+-- type Env :: (Type -> Type) -> Type+-- data Env m = Env+-- { _logger :: String -> m (),+-- _repository :: Int -> m (),+-- _controller :: Int -> m String+-- }+-- +-- instance HasLogger m (Env m) where+-- logger = _logger+-- +-- instance HasRepository m (Env m) where+-- repository = _repository+-- @+-- +-- Notice that the controller function is now part of the environment. No+-- favorites here!+-- +-- The following implementation of the logger function has no dependencies besides+-- 'MonadIO':+-- +-- @+-- mkStdoutLogger :: MonadIO m => String -> m ()+-- mkStdoutLogger msg = liftIO (putStrLn msg)+-- @+-- +-- But look at this implementation of the repository function. It gets hold of the+-- logger through @HasLogger@, just as the controller did:+-- +-- @+-- mkStdoutRepository :: (MonadDep '[HasLogger] d e m, MonadIO m) => Int -> m ()+-- mkStdoutRepository entity = do+-- e <- ask+-- liftD $ logger e "I'm going to write the entity!"+-- liftIO $ print entity+-- @+-- +-- It's about time we choose a concrete monad and assemble an environment record:+-- +-- @+-- envIO :: Env (DepT Env IO)+-- envIO =+-- let _logger = mkStdoutLogger+-- _repository = mkStdoutRepository+-- _controller = mkController+-- in Env {_logger, _repository, _controller}+-- @+-- +-- Not very complicated, except... what is that weird `DepT Env IO` doing there in+-- the signature? +-- +-- Well, that's the whole reason this library exists. For dependency injection to+-- work for *all* functions, @Env@ needs to be parameterized with a monad that+-- provides that same @Env@ environment. And trying to use a @ReaderT (Env+-- something) IO@ to parameterize @Env@ won't fly; you'll get weird "infinite+-- type" kind of errors. So I created the 'DepT' newtype over 'ReaderT' to mollify+-- the compiler.+-- +-- 'DepT' has 'MonadReader' and 'LiftDep' instances, so the effects of+-- @mkController@ can take place on it.+--+-- To invoke the controller from the environment, we can do something like+-- +-- @+-- runDepT (do e <- ask; _controller e 7) envIO +-- @+-- +-- or +-- +-- @+-- (do e <- ask; _controller e 7) `runDepT` envIO +-- @+-- +-- The companion package+-- [dep-t-advice](http://hackage.haskell.org/package/dep-t-advice) has some+-- helper functions for running 'DepT' computations.+ +-- $caveats+-- The structure of the 'DepT' type might be prone to trigger a [known infelicity+-- of the GHC+-- simplifier](https://twitter.com/DiazCarrete/status/1350116413445439493).++ -- $setup -- -- >>> :set -XTypeApplications@@ -70,7 +278,7 @@ -- | -- A monad transformer which adds a read-only environment to the given monad.--- The environment type must be parameterized with the transformer's stack.+-- The environment type must be parameterized with the transformer stack. -- -- The 'return' function ignores the environment, while @>>=@ passes the -- inherited environment to both subcomputations.@@ -129,6 +337,8 @@ runDepT = runReaderT . toReaderT -- |+-- Analogous to 'withReaderT'.+-- -- Changes the environment of a 'DepT', for example making the 'DepT' work in -- a "bigger" environment than the one in which was defined initially. --
lib/Control/Monad/Dep/Class.hs view
@@ -60,7 +60,8 @@ -- lifted using 'liftD' instead of 'lift' or 'liftIO'. -- module Control.Monad.Dep.Class- ( -- * Reader-like monads carrying dependencies in their environment+ ( + -- * Reader-like monads carrying dependencies in their environment MonadDep, -- * Lifting effects from dependencies
+ lib/Dep/Constructor.hs view
@@ -0,0 +1,302 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE ImportQualifiedPost #-}+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE BlockArguments #-}++-- | This module provides a method for performing dependency injection in 'Phased'+-- environments by means of building fixpoints.+--+-- To ease the reader into the concept, here's how we can define the+-- [@factorial@](https://en.wikibooks.org/wiki/Haskell/Fix_and_recursion#Recursion)+-- function using only 'fix' from @base@:+-- +-- >>> :{+-- type FactorialDeps = Int -> Int+-- makeFactorial :: FactorialDeps -> Int -> Int +-- makeFactorial deps n = if n == 0 then 1 else n * deps (n-1)+-- factorial :: Int -> Int+-- factorial = fix makeFactorial+-- :}+--+-- Looking at it, we can interpret it as a form of dependency injection. In the+-- example, @makeFactorial@ depends on another function of type @FactorialDeps@+-- for the @n > 1@ logic. So we build a fixpoint in which the resulting+-- \"closed\" @factorial@ is passed as the dependency to @makeFactorial@.+--+-- Very good, but what does this have to do with dependency injection in a+-- /real/ application? For real applications, we have a multitude of functions,+-- not only one. Each component has potentially many functions, and there may be+-- many components, with a complex directed acyclic graph of dependencies+-- between components!+--+-- This module provides the 'Constructor' applicative. A 'Phased' dependency+-- injection environment parameterized by 'Constructor' is like the set of all+-- component constructors taking part in dependency injection, each one still+-- \"open\" like @makeFactorial@, still waiting for its own dependencies.+--+-- And when we use the 'fixEnv' function on this \"open\" environment, we get+-- back a \"closed\" environment parameterized by 'Identity', were all the+-- dependencies have been resolved and the components are ready to be used.+module Dep.Constructor+ ( -- * Constructor phase+ Constructor,+ constructor,+ lmapConstructor,+ fixEnv,++ -- * Constructor with accumulator+ AccumConstructor,+ accumConstructor,+ accumConstructor_,+ _accumConstructor,+ _accumConstructor_,+ lmapAccumConstructor,+ fixEnvAccum,+ -- * "Control.Arrow" re-exports+ arr+ )+where++import Control.Applicative+import Data.Bifunctor (second)+import Data.Coerce+import Data.Function (fix)+import Data.Kind+import Data.Typeable+import Dep.Env hiding (AccumConstructor, Constructor, accumConstructor, constructor, fixEnv, fixEnvAccum)+import Control.Category (Category)+import Control.Category qualified+import Control.Arrow++-- | A phase with the effect of \"constructing each component by reading its+-- dependencies from a completed environment\". It should be the final phase.+-- +-- The @deps@ type parameter will typically be the \"closed\" form of the+-- dependency injection environment. That is, the type of environment produced+-- by 'fixEnv'. +newtype Constructor (deps :: Type) component+ = Constructor (deps -> component)+ deriving stock Functor++deriving newtype instance Category Constructor+-- | Mostly useful for 'arr', which builds a 'Constructor' out of a regular function.+deriving newtype instance Arrow Constructor+-- | 'pure' lifts a component that doesn't require any dependencies.+deriving newtype instance Applicative (Constructor deps)++-- | Turn an environment-consuming function into a 'Constructor' that can be slotted+-- into some field of a 'Phased' environment.+--+-- Equivalent to 'arr'.+constructor ::+ forall deps component.+ (deps -> component) ->+ Constructor deps component+-- same order of type parameters as Has+constructor = Constructor++-- | A generalized 'Constructor' which produces, in addition to the result+-- value, an @accum@ value which is then aggregated across all components and fed+-- back along with the completed environment.+--+-- Like 'Constructor', 'AccumConstructor' should be the final phase.+newtype AccumConstructor (accum :: Type) (deps :: Type) component+ = AccumConstructor ((accum, deps) -> (accum, component))+ deriving stock Functor++-- | 'pure' lifts a component that doesn't require any dependencies.+-- The produced accumulator will be 'mempty'.+instance Monoid accum => Applicative (AccumConstructor accum deps) where+ pure component = _accumConstructor_ \_ -> component+ liftA2 f (AccumConstructor u) (AccumConstructor v) = AccumConstructor \accumdeps ->+ let (acc1, component1) = u accumdeps+ (acc2, component2) = v accumdeps+ in (acc1 <> acc2, f component1 component2)++-- |+instance Monoid accum => Category (AccumConstructor accum) where+ id = _accumConstructor_ id+ (.) (AccumConstructor f) (AccumConstructor g) = AccumConstructor \(~(accum0,deps0)) -> + let (accum1, deps1) = g (accum0,deps0)+ (accum2, deps2) = f (accum0,deps1)+ in (accum1 <> accum2, deps2)++-- | Mostly useful for 'arr', which builds an 'AccumConstructor' out of a regular function. The produced accumulator will be 'mempty'.+instance Monoid accum => Arrow (AccumConstructor accum) where+ arr = _accumConstructor_+ first (AccumConstructor f) = AccumConstructor \(~(accum,(deps,extra))) -> + let (accum', component) = f (accum,deps)+ in (accum', (component, extra))++-- | Turn an environment-consuming function into an 'AccumConstructor' that can+-- be slotted into some field of a 'Phased' environment. The function also+-- consumes and produces a monoidal accumulator.+accumConstructor ::+ forall accum deps component.+ (accum -> deps -> (accum, component)) ->+ AccumConstructor accum deps component+accumConstructor f = AccumConstructor (\(~(accum, deps)) -> f accum deps)++accumConstructor_ ::+ forall accum deps component.+ Monoid accum =>+ -- | Consumes the accumulator but doesn't produce it (returns the 'mempty' accumulator.)+ (accum -> deps -> component) ->+ AccumConstructor accum deps component+accumConstructor_ f = accumConstructor $ \accum deps -> (mempty, f accum deps)++_accumConstructor ::+ forall accum deps component.+ -- | Doesn't consume the accumulator but produces it.+ (deps -> (accum, component)) ->+ AccumConstructor accum deps component+_accumConstructor f = accumConstructor $ \_ deps -> f deps++-- | Equivalent to 'arr'.+_accumConstructor_ ::+ forall accum deps component.+ Monoid accum =>+ -- | Neither consumes nor produces the accumulator, like a 'Constructor'.+ (deps -> component) ->+ AccumConstructor accum deps component+_accumConstructor_ f = accumConstructor $ \_ deps -> (mempty, f deps)++-- | This is a method of performing dependency injection by building fixpoints.+--+-- If we have a environment whose fields are functions that construct each+-- component by searching for its dependencies in a \"fully built\" version of+-- the environment, we can \"tie the knot\" to obtain the \"fully built\"+-- environment. This works as long as there aren't any circular dependencies+-- between components.+--+-- Think of it as a version of 'Data.Function.fix' that, instead of \"tying\" a single+-- function, ties a whole record of them.+--+-- We might have arrived as this \"ready-to-wire\" environment by peeling away+-- successive layers of applicative functor composition using 'pullPhase', until+-- only the wiring phase remains.+--+-- >>> :{+-- newtype Foo d = Foo {foo :: String -> d ()} deriving Generic+-- newtype Bar d = Bar {bar :: String -> d ()} deriving Generic+-- makeIOFoo :: MonadIO m => Foo m+-- makeIOFoo = Foo (liftIO . putStrLn)+-- makeBar :: Has Foo m env => env -> Bar m+-- makeBar (asCall -> call) = Bar (call foo)+-- type Deps_ = InductiveEnv [Bar,Foo]+-- type Deps = Deps_ Identity+-- deps_ :: Deps_ (Constructor (Deps IO)) IO+-- deps_ = EmptyEnv+-- & AddDep @Foo (constructor (\_ -> makeIOFoo))+-- & AddDep @Bar (constructor makeBar)+-- deps :: Deps IO+-- deps = fixEnv deps_+-- :}+--+-- >>> :{+-- bar (dep deps) "this is bar"+-- :}+-- this is bar+fixEnv ::+ (Phased deps_, Typeable deps_, Typeable m) =>+ -- | Environment where each field is wrapped in a 'Constructor'+ deps_ (Constructor (deps_ Identity m)) m ->+ -- | Fully constructed environment, ready for use.+ deps_ Identity m+fixEnv env = fix (pullPhase (liftAH decompose env))+ where+ decompose (Constructor f) = coerce f++-- | A generalized 'fixEnv' which threads a monoidal accumulator+-- along with the environment.+--+-- Sometimes, we need constructors to produce a monoidal value along with the+-- component. Think for example about some kind of composable startup action for+-- the component.+--+-- And on the input side, some constructors need access to the monoidal value+-- accumulated across all components. Think for example about a component which+-- publishes diagnostics coming from all other components.+fixEnvAccum ::+ (Phased deps_, Typeable deps_, Typeable m, Monoid accum, Typeable accum) =>+ -- | Environment where each field is wrapped in an 'AccumConstructor'+ deps_ (AccumConstructor accum (deps_ Identity m)) m ->+ -- | Fully constructed accumulator and environment, ready for use.+ (accum, deps_ Identity m)+fixEnvAccum env =+ let f = pullPhase <$> pullPhase (liftAH decompose env)+ in fix f+ where+ decompose (AccumConstructor f) = coerce f++-- | Change the dependency environment seen by the component.+lmapConstructor ::+ forall deps deps' component.+ Typeable component =>+ -- | Modifies the environment, with access to the 'TypeRep' of the component.+ (TypeRep -> deps -> deps') ->+ Constructor deps' component ->+ Constructor deps component+lmapConstructor tweak (Constructor f) =+ let tyRep = typeRep (Proxy @component)+ in Constructor $ f . tweak tyRep++-- | Change the dependency environment seen by the component.+--+-- The accumulator remains unchanged.+lmapAccumConstructor ::+ forall accum deps deps' component.+ Typeable component =>+ -- | Modifies the environment, with access to the 'TypeRep' of the component.+ (TypeRep -> deps -> deps') ->+ AccumConstructor accum deps' component ->+ AccumConstructor accum deps component+lmapAccumConstructor tweak (AccumConstructor f) =+ let tyRep = typeRep (Proxy @component)+ in AccumConstructor (\(~(accum, deps)) -> f (accum, tweak tyRep deps))++-- $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+-- >>> :set -XDerivingStrategies+-- >>> :set -XDerivingVia+-- >>> :set -XDeriveAnyClass+-- >>> :set -XStandaloneDeriving+-- >>> :set -XUndecidableInstances+-- >>> :set -XTypeOperators+-- >>> :set -XScopedTypeVariables+-- >>> import Data.Kind+-- >>> import Data.Function ((&))+-- >>> import Control.Monad.IO.Class+-- >>> import Dep.Has+-- >>> import Dep.Env hiding (AccumConstructor, Constructor, accumConstructor, constructor, fixEnv, fixEnvAccum)+-- >>> import GHC.Generics (Generic)
lib/Dep/Env.hs view
@@ -72,8 +72,14 @@ Autowired (..), Autowireable, + -- * Inductive environment with anonymous fields+ InductiveEnv (..),+ addDep,+ emptyEnv,+ -- * Managing phases Phased (..),+ liftAH, pullPhase, mapPhase, liftA2Phase,@@ -99,11 +105,6 @@ fixEnvAccum, AccumConstructor, - -- * Inductive environment with anonymous fields- InductiveEnv (..),- addDep,- emptyEnv,- -- * Re-exports Identity (..), Constant (..),@@ -151,11 +152,11 @@ -- >>> :set -XUndecidableInstances -- >>> :set -XTypeOperators -- >>> :set -XScopedTypeVariables+-- >>> :set -fno-warn-deprecations -- >>> import Data.Kind -- >>> import Data.Function ((&)) -- >>> import Control.Monad.IO.Class -- >>> import Dep.Env--- >>> import Dep.Env -- >>> import GHC.Generics (Generic) -- via the default field name@@ -169,7 +170,7 @@ -- 'Has', so maybe it doesn't make much sense to use it, except for -- explicitness. newtype TheDefaultFieldName (env :: Type) = TheDefaultFieldName env-+{-# DEPRECATED TheDefaultFieldName "more intrusive than useful" #-} instance (Dep r_, HasField (DefaultFieldName r_) (env_ m) u, Coercible u (r_ m)) => Has r_ m (TheDefaultFieldName (env_ m))@@ -275,7 +276,10 @@ -- outermost phase and running it in some way, until we are are left with a -- 'Constructor' phase, which we can remove using 'fixEnv'. ----- 'Phased' resembles [FunctorT, TraversableT and ApplicativeT](https://hackage.haskell.org/package/barbies-2.0.3.0/docs/Data-Functor-Transformer.html) from the [barbies](https://hackage.haskell.org/package/barbies) library. 'Phased' instances can be written in terms of them.+-- 'Phased' resembles [FunctorT, TraversableT and+-- ApplicativeT](https://hackage.haskell.org/package/barbies-2.0.3.0/docs/Data-Functor-Transformer.html)+-- from the [barbies](https://hackage.haskell.org/package/barbies) library,+-- although 'Phased' instances /can't/ be written in terms of them because of the extra 'Typeable' constraints. type Phased :: ((Type -> Type) -> (Type -> Type) -> Type) -> Constraint class Phased (env_ :: (Type -> Type) -> (Type -> Type) -> Type) where -- | Used to implement 'pullPhase' and 'mapPhase', typically you should use those functions instead.@@ -348,6 +352,16 @@ env_ f' m liftA2H f enva env = G.to (gLiftA2Phase f (G.from enva) (G.from env)) +-- | Slightly less powerful version of 'traverseH'.+liftAH ::+ forall deps_ phases phases' m.+ (Phased deps_, Typeable phases, Typeable phases', Typeable m) =>+ (forall x. Typeable x => phases x -> phases' x) ->+ deps_ phases m ->+ deps_ phases' m+liftAH tweak =+ runIdentity . traverseH (Identity . tweak)+ -- | Take the outermost phase wrapping each component and \"pull it outwards\", -- aggregating the phase's applicative effects. --@@ -632,10 +646,12 @@ -- -- The 'Constructor' phase for an environment will typically be parameterized -- with the environment itself.+{-# DEPRECATED Constructor "use the one in Dep.Constructor" #-} type Constructor (env :: Type) = ((->) env) `Compose` Identity -- | Turn an environment-consuming function into a 'Constructor' that can be slotted -- into some field of a 'Phased' environment.+{-# DEPRECATED constructor "use the one in Dep.Constructor" #-} constructor :: forall r_ m env. (env -> r_ m) -> Constructor env (r_ m) -- same order of type parameters as Has constructor = coerce@@ -645,6 +661,7 @@ -- back along with the completed environment. -- -- Like 'Constructor', 'AccumConstructor' should be the final phase.+{-# DEPRECATED AccumConstructor "use the one in Dep.Constructor" #-} type AccumConstructor (w :: Type) (env :: Type) = (->) (w, env) `Compose` (,) w `Compose` Identity -- | This is a method of performing dependency injection that doesn't require@@ -683,6 +700,7 @@ -- bar (dep envReady) "this is bar" -- :} -- this is bar+{-# DEPRECATED fixEnv "use the one in Dep.Constructor" #-} fixEnv :: (Phased env_, Typeable env_, Typeable m) => -- | Environment where each field is wrapped in a 'Constructor'@@ -706,6 +724,7 @@ -- (accumulator, environment) tuple needs to use a lazy pattern match like -- @~(w,env)@. Otherwise 'fixEnvAccum' enters an infinite loop! Such are the -- dangers of knot-tying.+{-# DEPRECATED fixEnvAccum "use the one in Dep.Constructor" #-} fixEnvAccum :: (Phased env_, Typeable env_, Typeable m, Monoid w, Typeable w) => -- | Environment where each field is wrapped in an 'AccumConstructor'
lib/Dep/Has.hs view
@@ -164,6 +164,7 @@ -- This allows defining 'Has' instances with empty bodies, thanks to -- @DefaultSignatures@. type Dep :: ((Type -> Type) -> Type) -> Constraint+{-# DEPRECATED Dep "more intrusive than useful" #-} class Dep r_ where -- The Char kind would be useful here, to lowercase the first letter of the -- k type and use it as the default preferred field name.
test/doctests.hs view
@@ -7,6 +7,7 @@ "lib/Control/Monad/Dep/Class.hs", "lib/Dep/Has.hs", "lib/Dep/Env.hs",+ "lib/Dep/Constructor.hs", "lib/Dep/Tagged.hs" ]
test/tests_env.hs view
@@ -29,7 +29,6 @@ module Main (main) where import Dep.Has-import Dep.Env import Control.Monad.Dep.Class import Control.Monad.Reader import Data.Functor.Constant@@ -43,6 +42,8 @@ import Prelude hiding (log) import Data.Functor.Identity import GHC.TypeLits+import Dep.Env hiding (AccumConstructor, Constructor, accumConstructor, constructor, fixEnv, fixEnvAccum)+import Dep.Constructor import Control.Monad.Trans.Cont import Data.Aeson import Data.Aeson.Types