dep-t-0.4.0.2: README.md
# dep-t
`DepT` is a
[ReaderT](http://hackage.haskell.org/package/mtl-2.2.2/docs/Control-Monad-Reader.html)-like
monad transformer for dependency injection.
The difference with `ReaderT` is that `DepT` takes an enviroment whose type is
parameterized by `DepT` itself.
## Rationale
To perform dependency injection in Haskell, a common solution is to build a
record of functions and pass it to the program logic using some variant of
[`ReaderT`](http://hackage.haskell.org/package/mtl-2.2.2/docs/Control-Monad-Reader.html).
To avoid becoming tied to a concrete reader environment, let's define some
auxiliary typeclasses that extract functions from a generic environment:
type HasLogger :: (Type -> Type) -> Type -> Constraint
class HasLogger d e | e -> d where
logger :: e -> String -> d ()
type HasRepository :: (Type -> Type) -> Type -> Constraint
class HasRepository d e | e -> d where
repository :: e -> Int -> d ()
We see that the type `e` of the environment determines the monad `d` on which
the effects take place.
Here's a monomorphic environment record with functions that have effects in `IO`:
type EnvIO :: Type
data EnvIO = EnvIO
{ _loggerIO :: String -> IO (),
_repositoryIO :: Int -> IO ()
}
instance HasLogger IO EnvIO where
logger = _loggerIO
instance HasRepository IO EnvIO where
repository = _repositoryIO
Record-of-functions-in-IO is a simple technique which works well in many
situations. There are even [specialized
libraries](http://hackage.haskell.org/package/rio) that support it.
Here's a function which can get its dependencies from the monomorphic
environment:
mkControllerIO :: (HasLogger IO e, HasRepository IO e) => Int -> ReaderT e IO String
mkControllerIO x = do
e <- ask
liftIO $ logger e "I'm going to insert in the db!"
liftIO $ repository e x
return "view"
That's all and well, but there are two issues that bug me:
- We might want to write code that is innocent of `IO` and polymorphic over the
monad, to ensure that the program logic can't do some unexpected missile
launch, or to allow testing our app in a "pure" way.
- What if the repository function needs access to the logger, too? The
repository lives in the environment record, but isn't aware of it. That means
it can't use the `HasLogger` typeclass for easy and convenient dependency
injection. Why privilege the controller in such a way?
In a sufficiently complex app, the diverse functions that comprise it will be
organized in a big
[DAG](https://en.wikipedia.org/wiki/Directed_acyclic_graph) of dependencies.
And it would be nice if all the functions taking part in dependency injection
were treated uniformly; if all of them had access to (some view of) the
environment record.
To tackle these issues, we begin by giving the controller a more general signature:
mkControllerIO :: (HasLogger IO e, HasRepository IO e, MonadIO m, MonadReader e m) => Int -> m String
Now the function can work in other reader-like monads besides `ReaderT`.
Let's go one step further, and abstract away the `IO`, so that functions in the
record can have effects in other monads:
mkController :: (HasLogger d e, HasRepository d e, LiftDep d m, MonadReader e m) => Int -> m String
mkController x = do
e <- ask
liftD $ logger e "I'm going to insert in the db!"
liftD $ repository e x
return "view"
Now both the signature and the implementation have changed:
- There's a new type variable `d`, the monad in which functions taken from the
environment `e` have their effects.
- `MonadIO` has been replaced by `LiftDep` from `Control.Monad.Dep.Class`, a
constraint that says we can lift `d` effects into `m` (though it could still
make sense to require `MonadIO m` for effects not originating in the
environment).
- Uses of `liftIO` have been replaced by `liftD`.
If all those constraints prove annoying to write, there's a convenient shorthand using the `MonadDep` type family:
mkController :: MonadDep [HasLogger, HasRepository] d e m => Int -> m String
The new, more polymorphic `mkController` function can replace the original `mkControllerIO`:
mkControllerIO' :: (HasLogger IO e, HasRepository IO e) => Int -> ReaderT e IO String
mkControllerIO' = mkController
Now let's focus on the environment record. We'll parameterize its type by a
monad:
type Env :: (Type -> Type) -> Type
data Env m = Env
{ _logger :: String -> m (),
_repository :: Int -> m (),
_controller :: Int -> m String
}
instance HasLogger m (Env m) where
logger = _logger
instance HasRepository m (Env m) where
repository = _repository
Notice that the controller function is now part of the environment. No
favorites here!
The following implementation of the logger function has no dependencies besides
`MonadIO`:
mkStdoutLogger :: MonadIO m => String -> m ()
mkStdoutLogger msg = liftIO (putStrLn msg)
But look at this implementation of the repository function. It gets hold of the
logger through `HasLogger`, just as the controller did:
mkStdoutRepository :: (MonadDep '[HasLogger] d e m, MonadIO m) => Int -> m ()
mkStdoutRepository entity = do
e <- ask
liftD $ logger e "I'm going to write the entity!"
liftIO $ print entity
It's about time we choose a concrete monad and assemble an environment record:
envIO :: Env (DepT Env IO)
envIO =
let _logger = mkStdoutLogger
_repository = mkStdoutRepository
_controller = mkController
in Env {_logger, _repository, _controller}
Not very complicated, except... what is that weird `DepT Env IO` doing there in
the signature?
Well, that's the whole reason this library exists. For dependency injection to
work for *all* functions, `Env` needs to be parameterized with a monad that
provides that same `Env` environment. And trying to use a `ReaderT (Env
something) IO` to parameterize `Env` won't fly; you'll get weird "infinite
type" kind of errors. So I created the `DepT` newtype over `ReaderT` to mollify
the compiler.
`DepT` has `MonadReader` and `LiftDep` instances, so the effects of
`mkController` can take place on it.
## So how do we invoke the controller now?
I suggest something like
runDepT (do e <- ask; _controller e 7) envIO
or
(do e <- ask; _controller e 7) `runDepT` envIO
The companion package
[dep-t-advice](http://hackage.haskell.org/package/dep-t-advice) has some more
functions for running `DepT` computations.
## How to avoid using "ask" and "liftD" before invoking a dependency?
One possible workaround (at the cost of more boilerplate) is to define helper
functions like:
loggerD :: MonadDep '[HasLogger] d e m => String -> m ()
loggerD msg = asks logger >>= \f -> liftD $ f msg
Which you can invoke like this:
usesLoggerD :: MonadDep [HasLogger, HasRepository] d e m => Int -> m String
usesLoggerD i = do
loggerD "I'm calling the logger!"
return "foo"
Though perhaps this isn't worth the hassle.
## How to use "pure fakes" during testing?
The [test suite](./test/tests.hs) has an example of using a `Writer` monad for
collecting the outputs of functions working as ["test
doubles"](https://martinfowler.com/bliki/TestDouble.html).
## 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
record seen by its own dependencies.
For example, the function might have a `HasLogger` constraint but we don't want
it to use the default `HasLogger` instance of the environment.
The companion package
[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?
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
The structure of the `DepT` type might be prone to trigger a [known infelicity
of the GHC
simplifier](https://twitter.com/DiazCarrete/status/1350116413445439493).
## Links
- This library was extracted from my answer to [this Stack Overflow
question](https://stackoverflow.com/a/61782258/1364288).
- The implementation of `mapDepT` was teased out in [this other SO question](https://stackoverflow.com/questions/65710657/writing-a-zooming-function-for-a-readert-like-monad-transformer).
- An [SO
answer](https://stackoverflow.com/questions/57703898/how-to-call-impure-functions-from-pure-ones/57714058#57714058)
about records-of-functions and the "veil of polymorphism".
- The answers to [this SO
question](https://stackoverflow.com/questions/61642492/simplifying-the-invocation-of-functions-stored-inside-an-readert-environment)
gave me the idea for how to "instrument" monadic functions (although the
original motive of the question was different).
- 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)
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 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
from the `ReaderT`-based one.