dep-t 0.5.1.0 → 0.6.0.0
raw patch · 10 files changed
+111/−39 lines, 10 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Dep.Tagged: Tagged :: r_ m -> Tagged s r_ m
+ Dep.Tagged: [unTagged] :: Tagged s r_ m -> r_ m
+ Dep.Tagged: instance forall k (s :: k) (r_ :: (* -> *) -> *) (m :: * -> *). GHC.Generics.Generic (Dep.Tagged.Tagged s r_ m)
+ Dep.Tagged: newtype Tagged s r_ m
+ Dep.Tagged: tagged :: forall s r_ m. r_ m -> Tagged s r_ m
+ Dep.Tagged: untag :: Tagged s r_ m -> r_ m
- Dep.Env: constructor :: forall r_ env_ m. (env_ Identity m -> r_ m) -> Constructor env_ m (r_ m)
+ Dep.Env: constructor :: forall r_ m env. (env -> r_ m) -> Constructor env (r_ m)
- Dep.Env: fixEnv :: (Phased env_, Typeable env_, Typeable m) => env_ (Constructor env_ m) m -> env_ Identity m
+ Dep.Env: fixEnv :: (Phased env_, Typeable env_, Typeable m) => env_ (Constructor (env_ Identity m)) m -> env_ Identity m
- Dep.Env: type Constructor (env_ :: (Type -> Type) -> (Type -> Type) -> Type) (m :: Type -> Type) = ((->) (env_ Identity m)) `Compose` Identity
+ Dep.Env: type Constructor (env :: Type) = ((->) env) `Compose` Identity
Files
- CHANGELOG.md +14/−0
- README.md +27/−17
- dep-t.cabal +2/−3
- lib/Control/Monad/Dep.hs +1/−1
- lib/Control/Monad/Dep/Env.hs +0/−5
- lib/Control/Monad/Dep/Has.hs +0/−5
- lib/Dep/Env.hs +4/−4
- lib/Dep/Tagged.hs +46/−0
- test/tests_env.hs +2/−2
- test/tests_has.hs +15/−2
CHANGELOG.md view
@@ -1,5 +1,19 @@ # Revision history for dep-t +## 0.6.0.0 + +* Added module `Dep.Tagged`. + +* Changed the `Constructor` type synonym. + + Now it takes a fully constructed environment type. + + This is a backwards-incompatible change. Type signatures might need to be modified, not so much term level code. + + https://github.com/danidiaz/dep-t/issues/17 + +* Removed deprecated modules. + ## 0.5.1.0 * `Control.Monad.Dep.Has` and `Control.Monad.Dep.Env` renamed as `Dep.Has` and `Dep.Env`.
README.md view
@@ -1,5 +1,21 @@ # 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. @@ -7,7 +23,7 @@ The difference with `ReaderT` is that `DepT` takes an enviroment whose type is parameterized by `DepT` itself. -## Rationale +### 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 @@ -166,16 +182,7 @@ `DepT` has `MonadReader` and `LiftDep` instances, so the effects of `mkController` can take place on it. -## Inter-module dependencies - -[](https://postimg.cc/xkpWPBTm) - -- __Control.Monad.Dep.Class__ can be used to program against both `ReaderT` and `DepT`. -- __Control.Monad.Dep__ contains the actual `DepT` monad transformer. -- __Dep.Has__ can be useful independently of `ReaderT`, `DepT` or any monad transformer. -- __Dep.Env__ provides extra definitions that help when building environments of records. - -## So how do we invoke the controller now? +### So how do we invoke the controller now? I suggest something like @@ -189,7 +196,7 @@ [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? +### 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: @@ -206,13 +213,13 @@ Though perhaps this isn't worth the hassle. -## How to use "pure fakes" during testing? +### 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). -## How to make a function "see" a different evironment from the one seen by its dependencies? +### How to make a function "see" a different evironment from the one seen by its dependencies? 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 @@ -225,14 +232,14 @@ [dep-t-advice](http://hackage.haskell.org/package/dep-t-advice) provides a `deceive` function that allows for this. -## How to add AOP-ish "aspects" to functions in an environment? +### How to add AOP-ish "aspects" to functions in an environment? 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. -## What if I don't want to use DepT, or any other monad transformer for that matter? +### What if I don't want to use DepT, or any other monad transformer for that matter? Check out the function `fixEnv` in module `Dep.Env`, which provides a transformer-less way to perform dependency injection, based on @@ -242,7 +249,7 @@ one that wraps each field, and another that works as the effect monad for the components. -## Caveats +### DepT caveats The structure of the `DepT` type might be prone to trigger a [known infelicity of the GHC @@ -342,3 +349,6 @@ This post recommends a "polymorphic record of functions" style, which fits the philosophy of this library. +- One [big disadvantage](https://www.reddit.com/r/haskell/comments/r6foxv/opinions_on_reader_continuationbased_io/hmthsoy/) of the records-of-functions approach: + + > representing effects as records of functions rather than typeclasses/fused effect invocations destroys inlining, so you’ll generate significantly worse Core if you use this on a hot path.
dep-t.cabal view
@@ -1,7 +1,7 @@ cabal-version: 3.0 name: dep-t -version: 0.5.1.0 +version: 0.6.0.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,10 +28,9 @@ import: common exposed-modules: Dep.Has Dep.Env + Dep.Tagged Control.Monad.Dep Control.Monad.Dep.Class - Control.Monad.Dep.Has - Control.Monad.Dep.Env hs-source-dirs: lib test-suite dep-t-test
lib/Control/Monad/Dep.hs view
@@ -11,7 +11,7 @@ {-# LANGUAGE KindSignatures #-} -- | --- This package provides 'DepT', a monad transformer similar to 'ReaderT'. +-- 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.
− lib/Control/Monad/Dep/Env.hs
@@ -1,5 +0,0 @@-module Control.Monad.Dep.Env {-# DEPRECATED "Renamed to Dep.Env" #-} ( - module Dep.Env - ) where - -import Dep.Env
− lib/Control/Monad/Dep/Has.hs
@@ -1,5 +0,0 @@-module Control.Monad.Dep.Has {-# DEPRECATED "Renamed to Dep.Has" #-} ( - module Dep.Has - ) where - -import Dep.Has
lib/Dep/Env.hs view
@@ -22,7 +22,7 @@ -- environments composed of records. -- -- It's not necessary when defining the record components themselves, in that --- case 'Control.Monad.Dep.Has' should suffice. +-- case "Dep.Has" should suffice. -- -- >>> :{ -- type Logger :: (Type -> Type) -> Type @@ -502,12 +502,12 @@ -- -- The 'Constructor' phase for an environment will typically be parameterized -- with the environment itself. -type Constructor (env_ :: (Type -> Type) -> (Type -> Type) -> Type) (m :: Type -> Type) = ((->) (env_ Identity m)) `Compose` Identity +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. -constructor :: forall r_ env_ m . (env_ Identity m -> r_ m) -> Constructor env_ m (r_ m) +constructor :: forall r_ m env . (env -> r_ m) -> Constructor env (r_ m) -- same order of type parameters as Has constructor = coerce @@ -527,7 +527,7 @@ -- The @env_ (Constructor env_ m) m@ parameter might be the result of peeling -- away successive layers of applicative functor composition using 'pullPhase', -- until only the wiring phase remains. -fixEnv :: (Phased env_, Typeable env_, Typeable m) => env_ (Constructor env_ m) m -> env_ Identity m +fixEnv :: (Phased env_, Typeable env_, Typeable m) => env_ (Constructor (env_ Identity m) ) m -> env_ Identity m fixEnv env = fix (pullPhase env) -- | An inductively constructed environment with anonymous fields.
+ lib/Dep/Tagged.hs view
@@ -0,0 +1,46 @@+{-# LANGUAGE ImportQualifiedPost #-} +{-# LANGUAGE StandaloneKindSignatures #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE PolyKinds #-} +{-# LANGUAGE ExplicitForAll #-} + +-- | Companion module to "Dep.Has" for disambiguanting record components within an environment. +-- +-- Similar in purpose to the [Qualifier annotation](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/annotation/Qualifier.html) in Java Spring. +-- +-- When using functions from "Dep.SimpleAdvice" (which tend to depend on coercions) with 'Tagged' components, remember to import the newtype's constructor. +module Dep.Tagged + ( + Tagged (..), + tagged, + untag + ) +where + +import Data.Kind +import Data.Typeable +import GHC.Generics qualified as G + +-- | Very similar to the @Data.Tagged@ type from the \"tagged\" package, but +-- with an extra monad type argument. The intended use is to disambiguate +-- record components within an environment, when there are multiple records of the same +-- type. +type Tagged :: k -> ((Type -> Type) -> Type) -> (Type -> Type) -> Type +newtype Tagged s r_ m = Tagged {unTagged :: r_ m} + deriving + ( + G.Generic, + Typeable + ) + +-- When inserting into an environment a component that you want to disambiguate, provide the the tag using a type variable and then supply the record value. +tagged :: forall s r_ m . r_ m -> Tagged s r_ m +tagged = Tagged + +-- | Alias for 'unTagged'. +-- +-- When invoking a method from a tagged dependency, +-- provide the tag using a type application and compose with the record selector. +untag :: Tagged s r_ m -> r_ m +untag = unTagged +
test/tests_env.hs view
@@ -168,9 +168,9 @@ type Allocator = ContT () IO -type Phases env_ m = Configurator `Compose` Allocator `Compose` Constructor env_ m +type Phases env = Configurator `Compose` Allocator `Compose` Constructor env -env :: EnvHKD (Phases EnvHKD IO) IO +env :: EnvHKD (Phases (EnvHKD Identity IO)) IO env = EnvHKD { logger = parseConf `bindPhase` \(LoggerConfiguration {messagePrefix}) ->
test/tests_has.hs view
@@ -54,6 +54,7 @@ import Data.Functor.Compose import Control.Exception hiding (TypeError) import System.IO +import Dep.Tagged import Data.Function -- https://stackoverflow.com/questions/53498707/cant-derive-generic-for-this-type/53499091#53499091 @@ -98,6 +99,16 @@ call insert [1, 2, 3, 4] return "view" +makeController2Loggers :: forall d e m. MonadDep [Has Logger, Has (Tagged "secondary" Logger), Has Repository] d e m => Controller m +makeController2Loggers = + 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] + call (log . untag @"secondary") "Logged again from secondary logger." + 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 -- useEnv f = do @@ -342,13 +353,15 @@ EmptyEnv & AddDep (skipPhase @Allocator $ Identity $ makeFakeLogger) + & AddDep (skipPhase @Allocator $ + Identity $ tagged @"secondary" makeFakeLogger) & AddDep (allocateMap `bindPhase` \_ -> Identity $ makeFakeRepository) & AddDep (skipPhase @Allocator $ - Identity $ makeController) + Identity $ makeController2Loggers) runContT (pullPhase @Allocator envInductive) \env -> do let r = execWriter $ runDepT (do env <- ask; serve (dep env) 7) env - assertEqual "" (["I'm going to insert in the db!","I'm going to select an entity","I'm going to write the entity!"],[1,2,3,4]) r + assertEqual "" (["I'm going to insert in the db!","I'm going to select an entity","I'm going to write the entity!", "Logged again from secondary logger."],[1,2,3,4]) r -- --