packages feed

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 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.--[![dep-t.png](https://i.postimg.cc/2j0qqkmJ/dep-t.png)](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