packages feed

dep-t 0.1.3.0 → 0.4.0.0

raw patch · 8 files changed

+565/−164 lines, 8 filesdep +doctestdep +sop-corePVP ok

version bump matches the API change (PVP)

Dependencies added: doctest, sop-core

API changes (from Hackage documentation)

- Control.Monad.Dep: instance Control.Monad.Cont.Class.MonadCont m => Control.Monad.Cont.Class.MonadCont (Control.Monad.Dep.DepT env m)
- Control.Monad.Dep: instance Control.Monad.Error.Class.MonadError e m => Control.Monad.Error.Class.MonadError e (Control.Monad.Dep.DepT env m)
- Control.Monad.Dep: instance Control.Monad.Fail.MonadFail m => Control.Monad.Fail.MonadFail (Control.Monad.Dep.DepT env m)
- Control.Monad.Dep: instance Control.Monad.Fix.MonadFix m => Control.Monad.Fix.MonadFix (Control.Monad.Dep.DepT env m)
- Control.Monad.Dep: instance Control.Monad.IO.Class.MonadIO m => Control.Monad.IO.Class.MonadIO (Control.Monad.Dep.DepT env m)
- Control.Monad.Dep: instance Control.Monad.IO.Unlift.MonadUnliftIO m => Control.Monad.IO.Unlift.MonadUnliftIO (Control.Monad.Dep.DepT env m)
- Control.Monad.Dep: instance Control.Monad.State.Class.MonadState s m => Control.Monad.State.Class.MonadState s (Control.Monad.Dep.DepT env m)
- Control.Monad.Dep: instance Control.Monad.Trans.Class.MonadTrans (Control.Monad.Dep.DepT env)
- Control.Monad.Dep: instance Control.Monad.Writer.Class.MonadWriter w m => Control.Monad.Writer.Class.MonadWriter w (Control.Monad.Dep.DepT env m)
- Control.Monad.Dep: instance Control.Monad.Zip.MonadZip m => Control.Monad.Zip.MonadZip (Control.Monad.Dep.DepT env m)
- Control.Monad.Dep: instance GHC.Base.Alternative m => GHC.Base.Alternative (Control.Monad.Dep.DepT env m)
- Control.Monad.Dep: instance GHC.Base.Applicative m => GHC.Base.Applicative (Control.Monad.Dep.DepT env m)
- Control.Monad.Dep: instance GHC.Base.Functor m => GHC.Base.Functor (Control.Monad.Dep.DepT env m)
- Control.Monad.Dep: instance GHC.Base.Monad m => Control.Monad.Reader.Class.MonadReader (env (Control.Monad.Dep.DepT env m)) (Control.Monad.Dep.DepT env m)
- Control.Monad.Dep: instance GHC.Base.Monad m => GHC.Base.Monad (Control.Monad.Dep.DepT env m)
- Control.Monad.Dep: instance GHC.Base.MonadPlus m => GHC.Base.MonadPlus (Control.Monad.Dep.DepT env m)
+ Control.Monad.Dep: instance (GHC.Base.Monad m, GHC.Types.Coercible newtyped (e_ (Control.Monad.Dep.DepT e_ m))) => Control.Monad.Dep.Class.LiftDep (Control.Monad.Dep.DepT e_ m) (Control.Monad.Trans.Reader.ReaderT newtyped m)
+ Control.Monad.Dep: instance Control.Monad.Cont.Class.MonadCont m => Control.Monad.Cont.Class.MonadCont (Control.Monad.Dep.DepT e_ m)
+ Control.Monad.Dep: instance Control.Monad.Error.Class.MonadError e m => Control.Monad.Error.Class.MonadError e (Control.Monad.Dep.DepT e_ m)
+ Control.Monad.Dep: instance Control.Monad.Fail.MonadFail m => Control.Monad.Fail.MonadFail (Control.Monad.Dep.DepT e_ m)
+ Control.Monad.Dep: instance Control.Monad.Fix.MonadFix m => Control.Monad.Fix.MonadFix (Control.Monad.Dep.DepT e_ m)
+ Control.Monad.Dep: instance Control.Monad.IO.Class.MonadIO m => Control.Monad.IO.Class.MonadIO (Control.Monad.Dep.DepT e_ m)
+ Control.Monad.Dep: instance Control.Monad.IO.Unlift.MonadUnliftIO m => Control.Monad.IO.Unlift.MonadUnliftIO (Control.Monad.Dep.DepT e_ m)
+ Control.Monad.Dep: instance Control.Monad.State.Class.MonadState s m => Control.Monad.State.Class.MonadState s (Control.Monad.Dep.DepT e_ m)
+ Control.Monad.Dep: instance Control.Monad.Trans.Class.MonadTrans (Control.Monad.Dep.DepT e_)
+ Control.Monad.Dep: instance Control.Monad.Writer.Class.MonadWriter w m => Control.Monad.Writer.Class.MonadWriter w (Control.Monad.Dep.DepT e_ m)
+ Control.Monad.Dep: instance Control.Monad.Zip.MonadZip m => Control.Monad.Zip.MonadZip (Control.Monad.Dep.DepT e_ m)
+ Control.Monad.Dep: instance GHC.Base.Alternative m => GHC.Base.Alternative (Control.Monad.Dep.DepT e_ m)
+ Control.Monad.Dep: instance GHC.Base.Applicative m => GHC.Base.Applicative (Control.Monad.Dep.DepT e_ m)
+ Control.Monad.Dep: instance GHC.Base.Functor m => GHC.Base.Functor (Control.Monad.Dep.DepT e_ m)
+ Control.Monad.Dep: instance GHC.Base.Monad m => Control.Monad.Dep.Class.LiftDep (Control.Monad.Dep.DepT e_ m) (Control.Monad.Dep.DepT e_ m)
+ Control.Monad.Dep: instance GHC.Base.Monad m => Control.Monad.Reader.Class.MonadReader (e_ (Control.Monad.Dep.DepT e_ m)) (Control.Monad.Dep.DepT e_ m)
+ Control.Monad.Dep: instance GHC.Base.Monad m => GHC.Base.Monad (Control.Monad.Dep.DepT e_ m)
+ Control.Monad.Dep: instance GHC.Base.MonadPlus m => GHC.Base.MonadPlus (Control.Monad.Dep.DepT e_ m)
+ Control.Monad.Dep.Class: class Monad d => LiftDep d m
+ Control.Monad.Dep.Class: instance GHC.Base.Monad m => Control.Monad.Dep.Class.LiftDep m (Control.Monad.Trans.Reader.ReaderT e m)
+ Control.Monad.Dep.Class: liftD :: LiftDep d m => d x -> m x
+ Control.Monad.Dep.Class: type family MonadDep dependencies d e m
- Control.Monad.Dep: DepT :: ReaderT (env (DepT env m)) m r -> DepT env m r
+ Control.Monad.Dep: DepT :: ReaderT (e_ (DepT e_ m)) m r -> DepT e_ m r
- Control.Monad.Dep: newtype DepT env m r
+ Control.Monad.Dep: newtype DepT e_ m r
- Control.Monad.Dep: runDepT :: DepT env m r -> env (DepT env m) -> m r
+ Control.Monad.Dep: runDepT :: DepT e_ m r -> e_ (DepT e_ m) -> m r
- Control.Monad.Dep: toReaderT :: DepT env m r -> ReaderT (env (DepT env m)) m r
+ Control.Monad.Dep: toReaderT :: DepT e_ m r -> ReaderT (e_ (DepT e_ m)) m r

Files

CHANGELOG.md view
@@ -1,5 +1,13 @@ # Revision history for dep-t
 
+## 0.4.0.0
+
+Actually no breaking changes here, but a change in the recommended structure of
+the HasX helper classes, and in how to write general code against those
+typeclasses.
+
+* added Control.Monad.Dep.Class
+
 ## 0.1.3.0
 
 * re-exported Control.Monad.Trans
README.md view
@@ -16,15 +16,16 @@ Let's start by defining some auxiliary typeclasses to extract functions from an
 environment record:
 
-    type HasLogger :: Type -> (Type -> Type) -> Constraint
-    class HasLogger r m | r -> m where
-      logger :: r -> String -> m ()
+    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 r m | r -> m where
-      repository :: r -> Int -> m ()
+    type HasRepository :: (Type -> Type) -> Type -> Constraint
+    class HasRepository d e | e -> d where
+      repository :: e -> Int -> d ()
 
-We see that the type of the record determines the monad in which the effects take place.
+We see that the type of the environment `e` determines the monad `d` on which
+the effects take place.
 
 Let's define a monomorphic record with effects in `IO`:
 
@@ -34,19 +35,20 @@         _repositoryIO :: Int -> IO ()
       }
 
-    instance HasLogger EnvIO IO where
+    instance HasLogger IO EnvIO where
       logger = _loggerIO
 
-    instance HasRepository EnvIO IO where
+    instance HasRepository IO EnvIO where
       repository = _repositoryIO
 
 Record-of-functions-in-IO 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 obtains its dependencies from the environment record:
+Here's a function which obtains its dependencies from the monomorphic
+environment:
 
-    mkControllerIO :: (HasLogger e IO, HasRepository e IO) => Int -> ReaderT e IO String
+    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!"
@@ -55,6 +57,10 @@ 
 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
@@ -67,25 +73,44 @@   were treated uniformly; if all of them had access to (some view of) the
   environment record.
 
-- 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. 
+To tackle these issues, let's begin by writing the controller in a more generic
+way:
 
-Let's parameterize our environment by a monad: 
+    mkController :: MonadDep [HasLogger, HasRepository] d 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"
 
+The signature has changed, and we now use `liftD` instead of `liftIO`. But
+`mkController` does the same things as `mkControllerIO`, and can be used as a
+substitute for it:
+
+    mkControllerIO' :: (HasLogger IO e, HasRepository IO e) => Int -> ReaderT e IO String
+    mkControllerIO' = mkController
+
+Here we have intantiated `m` to `ReaderT e IO`, and `d` (the effect monad used
+by dependencies in the environment) to `IO`. So in this example `liftD` (the
+function that lifts `d` effects to `m`) is simply `liftIO`.
+
+However the `MonadDep` constraint of `mkController` has valid instances for other
+monads, as we shall see.
+
+Now let's turn our attention to the environment record. Let's 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
       }
-    -- helper from the "rank2classes" package
-    $(Rank2.TH.deriveFunctor ''Env)
 
-    instance HasLogger (Env m) m where
+    instance HasLogger m (Env m) where
       logger = _logger
 
-    instance HasRepository (Env m) m where
+    instance HasRepository m (Env m) where
       repository = _repository
 
 Notice that the controller function is now part of the environment. No
@@ -98,24 +123,15 @@     mkStdoutLogger msg = liftIO (putStrLn msg)
 
 But look at this implementation of the repository function. It gets hold of the
-logger through `HasLogger`:
+logger through `HasLogger`, just as the controller did:
 
-    mkStdoutRepository :: (MonadReader e m, HasLogger e m, MonadIO m) => Int -> m ()
+    mkStdoutRepository :: (MonadDep '[HasLogger] d e m, MonadIO m) => Int -> m ()
     mkStdoutRepository entity = do
       e <- ask
-      logger e "I'm going to write the entity!"
+      liftD $ logger e "I'm going to write the entity!"
       liftIO $ print entity
 
-And here's the controller:
-
-    mkController :: (MonadReader e m, HasLogger e m, HasRepository e m) => Int -> m String
-    mkController x = do
-      e <- ask
-      logger e "I'm going to insert in the db!"
-      repository e x
-      return "view"
-
-Now, lets choose `IO` as the base monad and assemble an environment record:
+It's about time we chose a concrete monad and assemble an environment record:
 
     envIO :: Env (DepT Env IO)
     envIO =
@@ -127,121 +143,72 @@ 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. Trying to use a `ReaderT
-(Env something) IO` to parameterize `Env` won't fly; you'll get weird "infinite
-type" kind of errors because the `Env` needs to be parameterized with the monad
-that provides the `Env` environment. So I created the `DepT` newtype over
-`ReaderT` to mollify the compiler.
+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` satisfies the `MonadDep` constraint, 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 
 
-## How to embed environments into other environments?
-
-Sometimes it might be convenient to [nest an environment into another
-one](https://stackoverflow.com/questions/43452644/what-is-the-difference-between-spring-parent-context-and-child-context),
-basically making it a field of the bigger environment:
-
-    type BiggerEnv :: (Type -> Type) -> Type
-    data BiggerEnv m = BiggerEnv
-      { _inner :: Env m,
-        _extra :: Int -> m Int
-      }
-    $(Rank2.TH.deriveFunctor ''BiggerEnv)
-
-When constructing the bigger environment, we have to tweak the monad parameter
-of the smaller one, to make the types match. This can be done with the
-`zoomEnv` function:
-
-    biggerEnvIO :: BiggerEnv (DepT BiggerEnv IO)
-    biggerEnvIO =
-      let _inner' = zoomEnv (Rank2.<$>) _inner envIO
-          _extra = pure
-       in BiggerEnv {_inner = _inner', _extra}
-
-We need to pass as the first parameter of `zoomEnv` a function that tweaks the
-monad parameter of `Env` using a natural transformation. We can write such a
-function ourselves, but here we are using the function generated for us by the
-[rank2classes
-TH](http://hackage.haskell.org/package/rank2classes-1.4.1/docs/Rank2-TH.html#v:deriveFunctor).
+or 
 
-## How to use "pure fakes" during testing?
+    (do e <- ask; _controller e 7) `runDepT` envIO 
 
-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).
+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" or "asks" 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:  
 
-    logger' :: (MonadReader e m, HasLogger e m) => String -> m ()
-    logger' msg = asks logger >>= \f -> f msg
+    loggerD :: MonadDep '[HasLogger] d e m => String -> m ()
+    loggerD msg = asks logger >>= \f -> liftD $ f msg
 
 Which you can invoke like this:
 
-    mkController x = do
-      logger' "I'm going to insert in the db!"
-
-I'm not sure it's worth the hassle.
-
-## How to instrument functions in the environment?
-
-Once we have commited to a concrete monad and constructed our
-record-of-functions, we might indulge in a bit of low-calorie
-aspect-oriented-programming.
-
-For example, imagine we want a generic way of adding logging of function
-parameters to any function in the environment, provided the environment already
-contains a logging function.
-
-We can write the following typeclass:
+    usesLoggerD :: MonadDep [HasLogger, HasRepository] d e m => Int -> m String
+    usesLoggerD i = do
+      loggerD "I'm calling the logger!"
+      return "foo"
 
-    class Instrumentable e m r | r -> e m where
-      instrument ::
-        ( forall x.
-          HasLogger (e (DepT e m)) (DepT e m) =>
-          [String] ->
-          DepT e m x ->
-          DepT e m x
-        ) ->
-        r ->
-        r
+Though perhaps this isn't worth the hassle.
 
-Which means "if you tell me how to transform a terminal `DepT` action, using
-the list of preceding arguments, in an environment that has as logger, then
-I'll be able to transform any function which ends in `DepT`".
+## How to use "pure fakes" during testing?
 
-The terminal case is a `DepT` without preceding parameters:
+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).
 
-    instance HasLogger (e (DepT e m)) (DepT e m) => Instrumentable e m (DepT e m x) where
-      instrument f d = f [] d
+## How to make a function "see" a different evironment from the one seen by its dependencies?
 
-The recursive case handles functions argument by argument:
+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. 
 
-    instance (Instrumentable e m r, Show a) => Instrumentable e m (a -> r) where
-      instrument f ar =
-        let instrument' = instrument @e @m @r
-         in \a -> instrument' (\names d -> f (show a : names) d) (ar a)
+For example, the function might have a `HasLogger` constraint but we don't want
+it to use the default `HasLogger` instance of the environment.
 
-Here's how to add logging advice to the controller function:
+The companion package
+[dep-t-advice](http://hackage.haskell.org/package/dep-t-advice) provides a
+`deceive` function that allows for this.
 
-    instrumentedEnv :: Env (DepT Env (Writer TestTrace))
-    instrumentedEnv =
-       let loggingAdvice args action = do
-                e <- ask
-                logger e $ "advice before " ++ intercalate "," args
-                r <- action
-                logger e $ "advice after"
-                pure r
-        in env { _controller = instrument loggingAdvice (_controller env) }
+## How to add AOP-ish "aspects" to functions in an environment?
 
-More complete advice support can be found in the
-[dep-t-advice](http://hackage.haskell.org/package/dep-t-advice) package.
+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.
 
 ## Caveats
 
@@ -267,19 +234,39 @@ 
 - I'm unsure of the relationship between `DepT` and the technique described in
   [Adventures assembling records of
-  capabilities](https://discourse.haskell.org/t/adventures-assembling-records-of-capabilities/623). 
+  capabilities](https://discourse.haskell.org/t/adventures-assembling-records-of-capabilities/623)
+  which relies on having "open" and "closed" versions of the environment
+  record. 
 
   It seems that, with `DepT`, functions in the environment obtain their
   dependencies anew every time they are invoked. If we change a function in the
   environment record, all other functions which depend on it will be affected
   in subsequent invocations. I don't think this happens with "Adventures..." at
-  least when changing an already "assembled" record.
+  least when changing a "closed", already assembled record.
 
+  With `DepT` a function might use `local` if it knows enough about the
+  environment. That doesn't seem very useful for program logic; if fact it
+  sounds like a recipe for confusion. It could perhaps be useful for [AOP-ish
+  things](http://hackage.haskell.org/package/dep-t-advice), to keep a synthetic
+  "call stack", or to implement something like Logback's [Mapped Diagnostic
+  Context](http://logback.qos.ch/manual/mdc.html).
+
 - [RIO](http://hackage.haskell.org/package/rio) is a featureful ReaderT-like /
   prelude replacement library which favors monomorphic environments.
 
 - Another exploration of dependency injection with `ReaderT`:
   [ReaderT-OpenProduct-Environment](https://github.com/keksnicoh/ReaderT-OpenProduct-Environment).
+
+- The [van Laarhoven Free Monad](http://r6.ca/blog/20140210T181244Z.html).
+
+> Swierstra notes that by summing together functors representing primitive I/O
+> actions and taking the free monad of that sum, we can produce values use
+> multiple I/O feature sets. Values defined on a subset of features can be
+> lifted into the free monad generated by the sum. The equivalent process can
+> be performed with the van Laarhoven free monad by taking the product of
+> records of the primitive operations. Values defined on a subset of features
+> can be lifted by composing the van Laarhoven free monad with suitable
+> projection functions that pick out the requisite primitive operations. 
 
 - [registry](http://hackage.haskell.org/package/registry) is a package that
   implements an alternative approach to dependency injection, one different
dep-t.cabal view
@@ -1,7 +1,7 @@ cabal-version:       3.0
 
 name:                dep-t
-version:             0.1.3.0
+version:             0.4.0.0
 synopsis:            Reader-like monad transformer for dependency injection.
 description:         Put all your functions in the environment record! Let all
                      your functions read from the environment record! No favorites!
@@ -27,6 +27,7 @@ library
   import: common
   exposed-modules:     Control.Monad.Dep
+                       Control.Monad.Dep.Class
   hs-source-dirs:      lib 
 
 test-suite dep-t-test
@@ -40,3 +41,30 @@     template-haskell,
     tasty              >= 1.3.1,
     tasty-hunit        >= 0.10.0.2,
+
+test-suite dep-t-test-curry
+  import: common
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  main-is:             tests_curry.hs
+  build-depends:       
+    dep-t, 
+    rank2classes       ^>= 1.4.1,
+    template-haskell,
+    tasty              >=  1.3.1,
+    tasty-hunit        >=  0.10.0.2,
+    sop-core           ^>= 0.5.0.0,
+
+-- VERY IMPORTANT for doctests to work: https://stackoverflow.com/a/58027909/1364288
+-- http://hackage.haskell.org/package/cabal-doctest
+test-suite doctests
+  import:              common
+  ghc-options:         -threaded
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  main-is:             doctests.hs
+  build-depends:       
+                       dep-t, 
+                       rank2classes       ^>= 1.4.1,
+                       doctest            ^>= 0.17,
+
lib/Control/Monad/Dep.hs view
@@ -16,9 +16,7 @@ --    @DepT@'s own monad stack.
 --
 --    There's a function 'withDepT' which is analogous to 'withReaderT'.
---
---    There's no analogue of 'mapReaderT' however. This means you can't tweak
---    the monad below the 'DepT' with a natural transformation.
+--    There's no analogue of 'mapReaderT' however. 
 module Control.Monad.Dep
   ( 
     -- * The DepT transformer
@@ -27,9 +25,11 @@     toReaderT,
     withDepT,
     zoomEnv,
+    -- * The simplest environment
     NilEnv(NilEnv),
     -- * Re-exports
     module Control.Monad.Trans,
+    module Control.Monad.Dep.Class,
     module Control.Monad.Reader.Class
   )
 where
@@ -46,8 +46,23 @@ import Control.Monad.Trans.Identity
 import Control.Monad.Writer.Class
 import Control.Monad.Zip
+import Control.Monad.Dep.Class
 import Data.Kind (Type)
+import Data.Coerce
 
+-- $setup
+--
+-- >>> :set -XTypeApplications
+-- >>> :set -XImportQualifiedPost
+-- >>> :set -XTemplateHaskell
+-- >>> :set -XStandaloneKindSignatures
+-- >>> :set -XNamedFieldPuns
+-- >>> import Control.Monad.Dep
+-- >>> import Rank2 qualified
+-- >>> import Rank2.TH qualified
+--
+
+
 -- |
 --    A monad transformer which adds a read-only environment to the given monad.
 --    The environment type must be parameterized with the transformer's stack.
@@ -59,7 +74,7 @@   (Type -> Type) ->
   Type ->
   Type
-newtype DepT env m r = DepT {toReaderT :: ReaderT (env (DepT env m)) m r}
+newtype DepT e_ m r = DepT {toReaderT :: ReaderT (e_ (DepT e_ m)) m r}
   deriving
     ( Functor,
       Applicative,
@@ -72,21 +87,40 @@       MonadCont,
       MonadIO,
       MonadUnliftIO,
-      MonadReader (env (DepT env m))
+      MonadReader (e_ (DepT e_ m))
     )
 
-instance MonadTrans (DepT env) where
+instance MonadTrans (DepT e_) where
   lift = DepT . lift
 
-deriving instance MonadState s m => MonadState s (DepT env m)
+deriving instance MonadState s m => MonadState s (DepT e_ m)
 
-deriving instance MonadWriter w m => MonadWriter w (DepT env m)
+deriving instance MonadWriter w m => MonadWriter w (DepT e_ m)
 
-deriving instance MonadError e m => MonadError e (DepT env m)
+deriving instance MonadError e m => MonadError e (DepT e_ m)
 
+-- | 'DepT' can be d-lifted to a 'ReaderT' in which the environment record
+-- containing further 'DepT' actions has been hidden behind a newtype. 
+--
+-- This can be useful to "deceive" a function into using an environment
+-- possessing different instances than the instances seen by the function's
+-- dependencies.
+instance (Monad m, Coercible newtyped (e_ (DepT e_ m))) => LiftDep (DepT e_ m) (ReaderT newtyped m) where
+  liftD = coerce
+
+
+-- | 'DepT' can be d-lifted to itself.
+instance Monad m => LiftDep (DepT e_ m) (DepT e_ m) where
+  liftD = id
+
 -- |
 --    Runs a 'DepT' action in an environment.
-runDepT :: DepT env m r -> env (DepT env m) -> m r
+--      
+-- >>> runDepT (pure "foo") NilEnv
+-- "foo"
+--
+--    For more sophisticated invocation functions, see @runFinalDepT@ and @runFromEnv@ from <http://hackage.haskell.org/package/dep-t-advice dep-t-advice>.
+runDepT :: DepT e_ m r -> e_ (DepT e_ m) -> m r
 runDepT = runReaderT . toReaderT
 
 -- |
@@ -96,7 +130,7 @@ --    The scary first parameter is a function that, given a natural
 --    transformation of monads, changes the monad parameter of the environment
 --    record. This function can be defined manually for each environment record,
---    or it can be generated using TH from the "rank2classes" package.
+--    or it can be generated using TH from the <http://hackage.haskell.org/package/rank2classes-1.4.1/docs/Rank2-TH.html#v:deriveFunctor rank2classes> package.
 withDepT ::
   forall small big m a.
   Monad m =>
@@ -128,16 +162,42 @@ -- |
 --    Makes the functions inside a small environment require a bigger environment.
 --
---    This can be useful if we are encasing the small environment as a field of
---    the big environment, in order to make the types match.
---
 --    The scary first parameter is a function that, given a natural
 --    transformation of monads, changes the monad parameter of the environment
 --    record. This function can be defined manually for each environment record,
---    or it can be generated using TH from the "rank2classes" package.
-
--- For the reason for not inlining, see https://twitter.com/DiazCarrete/status/1350116413445439493
+--    or it can be generated using TH from the <http://hackage.haskell.org/package/rank2classes-1.4.1/docs/Rank2-TH.html#v:deriveFunctor rank2classes> package.
+--
+--    'zoomEnv' can be useful if we are encasing some preexisting small environment as a field of
+--    a big environment, in order to make the types match:
+--
+-- >>> :{ 
+--   type Env :: (Type -> Type) -> Type
+--   data Env m = Env
+--     { _logger :: String -> m (),
+--       _repository :: Int -> m (),
+--       _controller :: Int -> m String
+--     }
+--   $(Rank2.TH.deriveFunctor ''Env)
+--   env :: Env (DepT Env IO)
+--   env = Env 
+--     { _logger = \_ -> pure (), 
+--       _repository = \_ -> pure (), 
+--       _controller = \_ -> pure "foo" 
+--     }
+--   type BiggerEnv :: (Type -> Type) -> Type
+--   data BiggerEnv m = BiggerEnv
+--     { _inner :: Env m,
+--       _extra :: Int -> m Int
+--     }
+--   biggerEnv :: BiggerEnv (DepT BiggerEnv IO)
+--   biggerEnv = BiggerEnv 
+--     { _inner = zoomEnv (Rank2.<$>) _inner env, 
+--       _extra = pure
+--     }
+-- :}
+--
 {-# NOINLINE zoomEnv #-}
+-- For the reason for not inlining, see https://twitter.com/DiazCarrete/status/1350116413445439493
 zoomEnv ::
   forall small big m a.
   Monad m =>
+ lib/Control/Monad/Dep/Class.hs view
@@ -0,0 +1,125 @@+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveFunctor #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE FunctionalDependencies #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE ImportQualifiedPost #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE PolyKinds #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE StandaloneDeriving #-}
+{-# LANGUAGE StandaloneKindSignatures #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE TypeOperators #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+-- | This module provides definitions that let us generalize record-of-functions code of the form:
+--
+-- >>> :{ 
+--  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 ()
+--  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"
+-- :}
+--
+-- To the more general:
+--
+-- >>> :{
+--  mkController :: MonadDep [HasLogger, HasRepository] d 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"
+-- :}
+--
+-- The new code can be used as a drop-in replacement of the old one. Notice
+-- that in the new code effects taken from the environment record are
+-- lifted using 'liftD' instead of 'lift' or 'liftIO'. 
+--
+module Control.Monad.Dep.Class
+  ( -- * Reader-like monads carrying dependencies in their environment
+    MonadDep,
+
+    -- * Lifting effects from dependencies
+    LiftDep (..),
+  )
+where
+
+import Control.Monad.Reader
+import Data.Kind
+
+-- $setup
+--
+-- >>> :set -XTypeApplications
+-- >>> :set -XMultiParamTypeClasses
+-- >>> :set -XImportQualifiedPost
+-- >>> :set -XTemplateHaskell
+-- >>> :set -XStandaloneKindSignatures
+-- >>> :set -XNamedFieldPuns
+-- >>> :set -XFunctionalDependencies
+-- >>> :set -XFlexibleContexts
+-- >>> :set -XDataKinds
+-- >>> import Control.Monad.Dep
+-- >>> import Rank2 qualified
+-- >>> import Rank2.TH qualified
+--
+
+
+-- | Auxiliary typeclass for monads that can be lifted to other monads.
+--
+-- Its intended use is lifting monadic actions found in some reader-like
+-- environment.
+--
+-- __Note:__ The @RIO@ monad from <http://hackage.haskell.org/package/rio rio> could be
+-- given a @LiftDept IO RIO@ instance; it's not done here to avoid increasing
+-- the dependency footprint.
+type LiftDep :: (Type -> Type) -> (Type -> Type) -> Constraint
+class Monad d => LiftDep d m where
+  liftD :: d x -> m x
+
+-- | The simplest case: we can d-lift the base monad sitting just below a 'ReaderT'.
+--
+-- (Perhaps this could be extended to any monad transformer, but let's leave it simple for now.)
+instance Monad m => LiftDep m (ReaderT e m) where
+  liftD = lift
+
+-- Trying to write a general "lift from anywhere in the stack" like
+-- instance (Monad m, LiftDep below m, MonadTrans t) => LiftDep below (t m)
+-- caused OverlappedInstances problems with DepT.
+-- Lot let's not try to be *excessively* general.
+
+-- -- | Any monad can be \"lifted\" to itself.
+-- Better limit this to DepT
+-- instance LiftDep m m where
+--   liftD = id
+
+-- | 'MonadDep' is not its own typeclass, but 'MonadReader' plus some supplementary constraints. 
+--
+-- A @MonadDep dependencies d e m@ is a @MonadReader e m@ where the environment
+-- @e@ provides some @dependencies@ with effects in the monad @d@, additionally requiring that the 
+-- effects can be lifted back to the monad @m@ by using 'liftD'.
+--
+-- The @dependencies@ are specified as a type-level list of two-parameter
+-- @HasX@-style typeclasses. Those typeclasses should expect the effect monad
+-- @d@ as its first parameter.
+--
+-- Writing code polymorphic over 'MonadDep' lets us execute it in both 'ReaderT' and 'Control.Monad.Dep.DepT' contexts.
+type MonadDep ::
+  [(Type -> Type) -> Type -> Constraint] ->
+  (Type -> Type) ->
+  Type ->
+  (Type -> Type) ->
+  Constraint
+type family MonadDep dependencies d e m where
+  MonadDep '[] d e m = (LiftDep d m, MonadReader e m)
+  MonadDep (dependency ': dependencies) d e m = (dependency d e, MonadDep dependencies d e m)
+ test/doctests.hs view
@@ -0,0 +1,5 @@+module Main (main) where
+
+import Test.DocTest
+main = doctest ["-ilib", "lib/Control/Monad/Dep.hs", "lib/Control/Monad/Dep/Class.hs"]
+
test/tests.hs view
@@ -10,6 +10,7 @@ {-# LANGUAGE TemplateHaskell #-}
 {-# LANGUAGE TypeApplications #-}
 {-# LANGUAGE UndecidableInstances #-}
+{-# LANGUAGE DataKinds #-}
 
 module Main (main) where
 
@@ -17,6 +18,7 @@ import Control.Monad.Reader
 import Control.Monad.Writer
 import Data.Kind
+import Data.Coerce
 import Data.List (intercalate)
 import Rank2 qualified
 import Rank2.TH qualified
@@ -29,40 +31,48 @@ -- Has-style typeclasses can be provided to avoid depending on concrete
 -- environments.
 -- Note that the environment determines the monad.
-type HasLogger :: Type -> (Type -> Type) -> Constraint
-class HasLogger r m | r -> m where
-  logger :: r -> String -> m ()
+type HasLogger :: (Type -> Type) -> Type -> Constraint
+class HasLogger d e | e -> d where
+  logger :: e -> String -> d ()
 
 -- Possible convenience function to avoid having to use ask before logging
 -- Worth the extra boilerplate, or not?
-logger' :: (MonadReader e m, HasLogger e m) => String -> m ()
-logger' msg = asks logger >>= \f -> f msg
+--logger' :: (MonadReader e m, LiftDep d m, HasLogger e d) => String -> m ()
+loggerD :: MonadDep '[HasLogger] d e m => String -> m ()
+loggerD msg = asks logger >>= \f -> liftD $ f msg
 
-type HasRepository :: Type -> (Type -> Type) -> Constraint
-class HasRepository r m | r -> m where
-  repository :: r -> Int -> m ()
+type HasRepository :: (Type -> Type) -> Type -> Constraint
+class HasRepository d e | e -> d where
+  repository :: e -> Int -> d ()
 
 -- Some possible implementations.
 --
 -- An implementation of the controller, done programming against interfaces
 -- (well, against typeclasses).
 -- Polymorphic on the monad.
-mkController :: (MonadReader e m, HasLogger e m, HasRepository e m) => Int -> m String
+--mkController :: (MonadReader e m, LiftDep d m, HasLogger e d, HasRepository e d) => Int -> m String
+mkController :: MonadDep '[HasLogger, HasRepository] d e m => Int -> m String
 mkController x = do
   e <- ask
-  logger e "I'm going to insert in the db!"
-  repository e x
+  liftD $ logger e "I'm going to insert in the db!"
+  liftD $ repository e x
   return "view"
 
+usesLoggerD :: MonadDep '[HasLogger, HasRepository] d e m => Int -> m String
+usesLoggerD i = do
+  loggerD "I'm calling the logger!"
+  return "foo"
+
 -- A "real" logger implementation that interacts with the external world.
 mkStdoutLogger :: MonadIO m => String -> m ()
 mkStdoutLogger msg = liftIO (putStrLn msg)
 
 -- A "real" repository implementation
-mkStdoutRepository :: (MonadReader e m, HasLogger e m, MonadIO m) => Int -> m ()
+--mkStdoutRepository :: (MonadReader e m, LiftDep d m, HasLogger e d, MonadIO m) => Int -> m ()
+mkStdoutRepository :: (MonadDep '[HasLogger] d e m, MonadIO m) => Int -> m ()
 mkStdoutRepository entity = do
   e <- ask
-  logger e "I'm going to write the entity!"
+  liftD $ logger e "I'm going to write the entity!"
   liftIO $ print entity
 
 -- The traces we accumulate from the fakes during tests
@@ -73,10 +83,11 @@ mkFakeLogger msg = tell ([msg], [])
 
 -- Ditto.
-mkFakeRepository :: (MonadReader e m, HasLogger e m, MonadWriter TestTrace m) => Int -> m ()
+--mkFakeRepository :: (MonadReader e m, LiftDep d m, HasLogger e d, MonadWriter TestTrace m) => Int -> m ()
+mkFakeRepository :: (MonadDep '[HasLogger] d e m, MonadWriter TestTrace m) => Int -> m ()
 mkFakeRepository entity = do
   e <- ask
-  logger e "I'm going to write the entity!"
+  liftD $ logger e "I'm going to write the entity!"
   tell ([], [entity])
 
 --
@@ -88,12 +99,25 @@     _repositoryIO :: Int -> IO ()
   }
 
-instance HasLogger EnvIO IO where
+envIO' :: EnvIO
+envIO' =
+  let _loggerIO = mkStdoutLogger
+      _repositoryIO i = print "this is the repo"
+   in EnvIO {_loggerIO, _repositoryIO}
+
+instance HasLogger IO EnvIO where
   logger = _loggerIO
 
-instance HasRepository EnvIO IO where
+instance HasRepository IO EnvIO where
   repository = _repositoryIO
 
+-- mkController works both with DepT and with ReaderT + monomorphic environment
+runningTheControllerInReaderT :: IO String
+runningTheControllerInReaderT = mkController 5 `runReaderT` envIO'
+
+mkControllerIO' :: (HasLogger IO e, HasRepository IO e) => Int -> ReaderT e IO String
+mkControllerIO' = mkController
+
 -- In the monomorphic environment, the controller function lives "separate",
 -- having access to the logger and the repository through the ReaderT
 -- environment.
@@ -105,7 +129,9 @@ -- In a sufficiently complex app, the diverse functions will form a DAG of
 -- dependencies between each other. So it would be nice if the functions were
 -- treated uniformly, all having access to (views of) the environment record.
-mkControllerIO :: (HasLogger e IO, HasRepository e IO) => Int -> ReaderT e IO String
+--
+-- no need for this now that we have MonadDep... just use the conventional mkController
+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!"
@@ -127,10 +153,10 @@ 
 -- If our environment is parmeterized by the monad m, then logging is done in
 -- m.
-instance HasLogger (Env m) m where
+instance HasLogger m (Env m) where
   logger = _logger
 
-instance HasRepository (Env m) m where
+instance HasRepository m (Env m) where
   repository = _repository
 
 -- This bigger environment is for demonstrating how to "nest" environments.
@@ -190,7 +216,7 @@ class Instrumentable e m r | r -> e m where
   instrument ::
     ( forall x.
-      HasLogger (e (DepT e m)) (DepT e m) =>
+      HasLogger (DepT e m) (e (DepT e m)) =>
       [String] ->
       DepT e m x ->
       DepT e m x
@@ -198,7 +224,7 @@     r ->
     r
 
-instance HasLogger (e (DepT e m)) (DepT e m) => Instrumentable e m (DepT e m x) where
+instance HasLogger (DepT e m) (e (DepT e m)) => Instrumentable e m (DepT e m x) where
   instrument f d = f [] d
 
 instance (Instrumentable e m r, Show a) => Instrumentable e m (a -> r) where
@@ -219,6 +245,7 @@ expectedInstrumented :: TestTrace
 expectedInstrumented = (["advice before: 7", "I'm going to insert in the db!", "I'm going to write the entity!", "advice after"], [7])
 
+
 --
 --
 --
@@ -227,6 +254,25 @@ --
 --
 --
+
+--
+--
+-- checking that coerce works on environments...
+newtype NilEnv' m = NilEnv' (NilEnv m)
+
+toBeCoerced :: DepT NilEnv IO ()
+toBeCoerced = pure ()
+
+toBeCoerced' :: DepT NilEnv' IO ()
+toBeCoerced' = coerce toBeCoerced
+
+toBeCoerced'' :: DepT NilEnv IO ()
+toBeCoerced'' = coerce toBeCoerced'
+--
+--
+--
+
+
 tests :: TestTree
 tests =
   testGroup
@@ -236,7 +282,10 @@           execWriter $ runDepT (do e <- ask; (_controller . _inner) e 7) biggerEnv,
       testCase "hopeAOPWorks" $
         assertEqual "" expectedInstrumented $
-          execWriter $ runDepT (do e <- ask; _controller e 7) instrumentedEnv
+          execWriter $ runDepT (do e <- ask; _controller e 7) instrumentedEnv,
+      testCase "hopeLoggerDWorks" $
+        assertEqual "" (["foo"],[]) $
+          execWriter $ loggerD "foo" `runDepT` env
     ]
 
 main :: IO ()
+ test/tests_curry.hs view
@@ -0,0 +1,139 @@+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE FunctionalDependencies #-}
+{-# LANGUAGE ImportQualifiedPost #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE StandaloneKindSignatures #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeApplications #-}
+{-# LANGUAGE UndecidableInstances #-}
+{-# LANGUAGE TypeOperators #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+module Main (main) where
+
+import Control.Monad.Dep
+import Control.Monad.Reader
+import Control.Monad.Writer
+import Data.Kind
+import Data.Coerce
+import Data.List (intercalate)
+import Rank2 qualified
+import Rank2.TH qualified
+import Test.Tasty
+import Test.Tasty.HUnit
+import Prelude hiding (log)
+import Data.SOP
+import Data.SOP.NP
+
+type Multicurryable ::
+  [Type] ->
+  ((Type -> Type) -> Type) ->
+  (Type -> Type) ->
+  Type ->
+  Type ->
+  Constraint
+class Multicurryable as e m r curried | curried -> as e m r where
+  type DownToBaseMonad as e m r curried :: Type
+  multiuncurry :: curried -> NP I as -> DepT e m r
+  multicurry :: (NP I as -> DepT e m r) -> curried
+  _runFromEnv :: m (e (DepT e m)) -> (e (DepT e m) -> curried) -> DownToBaseMonad as e m r curried
+
+instance Monad m => Multicurryable '[] e m r (DepT e m r) where
+  type DownToBaseMonad '[] e m r (DepT e m r) = m r
+  multiuncurry action Nil = action
+  multicurry f = f Nil
+  _runFromEnv producer extractor = do
+    e <- producer
+    runDepT (extractor e) e
+
+instance Multicurryable as e m r curried => Multicurryable (a ': as) e m r (a -> curried) where
+  type DownToBaseMonad (a ': as) e m r (a -> curried) = a -> DownToBaseMonad as e m r curried
+  multiuncurry f (I a :* as) = multiuncurry @as @e @m @r @curried (f a) as
+  multicurry f a = multicurry @as @e @m @r @curried (f . (:*) (I a))
+  _runFromEnv producer extractor a = _runFromEnv @as @e @m @r @curried producer (\f -> extractor f a)
+
+class Multicurryable as e m r curried => Deceivable as newtyped e m r curried where
+  type Deceive as newtyped e m r curried :: Type  
+  deceive :: (e (DepT e m) -> newtyped) -> Deceive as newtyped e m r curried -> curried 
+
+instance Monad m => Deceivable '[] newtyped e m r (DepT e m r) where
+  type Deceive '[] newtyped e m r (DepT e m r) = ReaderT newtyped m r
+  deceive f action = DepT (withReaderT f action)
+
+instance Deceivable as newtyped e m r curried => Deceivable (a ': as) newtyped e m r (a -> curried) where
+  type Deceive (a ': as) newtyped e m r (a -> curried) = a -> Deceive as newtyped e m r curried
+  deceive f g a = deceive @as @newtyped @e @m @r f (g a)
+
+--
+--
+--
+type HasLogger :: (Type -> Type) -> Type -> Constraint
+class HasLogger m em | em -> m where
+  logger :: em -> String -> m ()
+type HasIntermediate :: (Type -> Type) -> Type -> Constraint
+class HasIntermediate m em | em -> m where
+  intermediate :: em -> String -> m ()
+type Env :: (Type -> Type) -> Type
+data Env m = Env
+  { _logger1 :: String -> m (),
+    _logger2 :: String -> m (),
+    _intermediate :: String -> m (),
+    _controllerA :: Int -> m (),
+    _controllerB :: Int -> m ()
+  }
+instance HasLogger m (Env m) where
+  logger = _logger1
+instance HasIntermediate m (Env m) where
+  intermediate = _intermediate
+
+newtype Switcheroo m = Switcheroo (Env m) 
+    deriving newtype (HasIntermediate m)
+instance HasLogger m (Switcheroo m) where
+  logger (Switcheroo e) = _logger2 e
+
+envW :: Env (DepT Env (Writer [String]))
+envW = Env 
+  {
+    _logger1 = 
+       \_ -> tell ["logger 1"],
+    _logger2 = 
+       \_ -> tell ["logger 2"],
+    _intermediate =
+       \_ -> do e <- ask ; liftD $ logger e "foo", 
+    _controllerA = 
+       \_ -> do e <- ask; liftD $ logger e "foo" ; liftD $ intermediate e "foo",
+    _controllerB = 
+       deceive Switcheroo $
+       \_ -> do e <- ask; liftD $ logger e "foo" ; liftD $ intermediate e "foo"
+  }
+
+_controllerA' :: MonadDep '[HasLogger, HasIntermediate] d e m => String -> m () 
+_controllerA' = \_ -> do e <- ask; liftD $ logger e "foo" ; liftD $ intermediate e "foo"
+
+_intermediate' :: MonadDep '[HasLogger] d e m => String -> m () 
+_intermediate' = \_ -> do e <- ask ; liftD $ logger e "foo" 
+
+--
+--
+tests :: TestTree
+tests =
+  testGroup
+    "All"
+    [
+      testCase "undeceived" $
+        assertEqual "" ["logger 1", "logger 1"] $
+          execWriter $ (do e <- ask; _controllerA e 7) `runDepT` envW,
+      testCase "deceived" $
+        assertEqual "" ["logger 2", "logger 1"] $
+          execWriter $ (do e <- ask; _controllerB e 7) `runDepT` envW
+    ]
+
+main :: IO ()
+main = defaultMain tests