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 +8/−0
- README.md +110/−123
- dep-t.cabal +29/−1
- lib/Control/Monad/Dep.hs +77/−17
- lib/Control/Monad/Dep/Class.hs +125/−0
- test/doctests.hs +5/−0
- test/tests.hs +72/−23
- test/tests_curry.hs +139/−0
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