Safe Haskell | Safe-Inferred |
---|---|
Language | Haskell2010 |
Synopsis
- newtype DepT (e_ :: (Type -> Type) -> Type) (m :: Type -> Type) (r :: Type) = DepT (ReaderT (e_ (DepT e_ m)) m r)
- runDepT :: DepT e_ m r -> e_ (DepT e_ m) -> m r
- toReaderT :: DepT e_ m r -> ReaderT (e_ (DepT e_ m)) m r
- withDepT :: forall small big m a. Monad m => (forall p q. (forall x. p x -> q x) -> small p -> small q) -> (forall t. big t -> small t) -> DepT small m a -> DepT big m a
- zoomEnv :: forall small big m a. Monad m => (forall p q. (forall x. p x -> q x) -> small p -> small q) -> (forall t. big t -> small t) -> small (DepT small m) -> small (DepT big m)
- data NilEnv m = NilEnv
- newtype Constant a (b :: k) = Constant {
- getConstant :: a
- module Control.Monad.Trans
- module Control.Monad.Dep.Class
- module Control.Monad.Reader.Class
Motivation
Dependency injection.
To perform dependency injection in Haskell, a common solution is to build a
record of functions and pass it to the program logic using some variant of
ReaderT
.
To avoid becoming tied to a concrete reader environment, let's define some auxiliary typeclasses that extract functions from a generic environment:
type HasLogger :: (Type -> Type) -> Type -> Constraint class HasLogger d e | e -> d where logger :: e -> String -> d () type HasRepository :: (Type -> Type) -> Type -> Constraint class HasRepository d e | e -> d where repository :: e -> Int -> d ()
We see that the type e
of the environment determines the monad d
on which
the effects take place.
Here's a monomorphic environment record with functions that have effects in IO
:
type EnvIO :: Type data EnvIO = EnvIO { _loggerIO :: String -> IO (), _repositoryIO :: Int -> IO () } instance HasLogger IO EnvIO where logger = _loggerIO instance HasRepository IO EnvIO where repository = _repositoryIO
Record-of-functions-in-IO is a simple technique which works well in many situations. There are even specialized libraries 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 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 environmente
have their effects. MonadIO
has been replaced byLiftDep
fromClass
, a constraint that says we can liftd
effects intom
(though it could still make sense to require `MonadIO m` for effects not originating in the environment).- Uses of
liftIO
have been replaced byliftD
.
If all those constraints prove annoying to write, there's a convenient shorthand using the MonadDep
type family:
MonadDep [HasLogger, HasRepository] d e m => Int -> m String
The new, more polymorphic mkController
function can replace the original mkControllerIO
:
mkControllerIO' :: (HasLogger IO e, HasRepository IO e) => Int -> ReaderT e IO String mkControllerIO' = mkController
Now let's focus on the environment record. We'll parameterize its type by a monad:
type Env :: (Type -> Type) -> Type data Env m = Env { _logger :: String -> m (), _repository :: Int -> m (), _controller :: Int -> m String } instance HasLogger m (Env m) where logger = _logger instance HasRepository m (Env m) where repository = _repository
Notice that the controller function is now part of the environment. No favorites here!
The following implementation of the logger function has no dependencies besides
MonadIO
:
mkStdoutLogger :: MonadIO m => String -> m () mkStdoutLogger msg = liftIO (putStrLn msg)
But look at this implementation of the repository function. It gets hold of the
logger through HasLogger
, just as the controller did:
mkStdoutRepository :: (MonadDep '[HasLogger] d e m, MonadIO m) => Int -> m () mkStdoutRepository entity = do e <- ask liftD $ logger e "I'm going to write the entity!" liftIO $ print entity
It's about time we choose a concrete monad and assemble an environment record:
envIO :: Env (DepT Env IO) envIO = let _logger = mkStdoutLogger _repository = mkStdoutRepository _controller = mkController in Env {_logger, _repository, _controller}
Not very complicated, except... what is that weird `DepT Env IO` doing there in the signature?
Well, that's the whole reason this library exists. For dependency injection to
work for *all* functions, Env
needs to be parameterized with a monad that
provides that same Env
environment. And trying to use a ReaderT (Env
something) IO
to parameterize Env
won't fly; you'll get weird "infinite
type" kind of errors. So I created the DepT
newtype over ReaderT
to mollify
the compiler.
DepT
has MonadReader
and LiftDep
instances, so the effects of
mkController
can take place on it.
To invoke the controller from the environment, we can do something like
runDepT (do e <- ask; _controller e 7) envIO
or
(do e <- ask; _controller e 7) runDepT
envIO
The companion package
dep-t-advice has some
helper functions for running DepT
computations.
Caveats
The structure of the DepT
type might be prone to trigger a known infelicity
of the GHC
simplifier.
The DepT transformer
newtype DepT (e_ :: (Type -> Type) -> Type) (m :: Type -> Type) (r :: Type) Source #
A monad transformer which adds a read-only environment to the given monad. The environment type must be parameterized with the transformer stack.
The return
function ignores the environment, while >>=
passes the
inherited environment to both subcomputations.
Instances
MonadError e m => MonadError e (DepT e_ m) Source # | |
Defined in Control.Monad.Dep throwError :: e -> DepT e_ m a # catchError :: DepT e_ m a -> (e -> DepT e_ m a) -> DepT e_ m a # | |
MonadState s m => MonadState s (DepT e_ m) Source # | |
MonadWriter w m => MonadWriter w (DepT e_ m) Source # | |
MonadTrans (DepT e_) Source # | |
Defined in Control.Monad.Dep | |
Monad m => MonadReader (e_ (DepT e_ m)) (DepT e_ m) Source # | |
MonadFail m => MonadFail (DepT e_ m) Source # | |
Defined in Control.Monad.Dep | |
MonadFix m => MonadFix (DepT e_ m) Source # | |
Defined in Control.Monad.Dep | |
MonadIO m => MonadIO (DepT e_ m) Source # | |
Defined in Control.Monad.Dep | |
MonadZip m => MonadZip (DepT e_ m) Source # | |
Alternative m => Alternative (DepT e_ m) Source # | |
Applicative m => Applicative (DepT e_ m) Source # | |
Functor m => Functor (DepT e_ m) Source # | |
Monad m => Monad (DepT e_ m) Source # | |
MonadPlus m => MonadPlus (DepT e_ m) Source # | |
MonadCont m => MonadCont (DepT e_ m) Source # | |
MonadUnliftIO m => MonadUnliftIO (DepT e_ m) Source # | |
Defined in Control.Monad.Dep | |
Monad m => LiftDep (DepT e_ m) (DepT e_ m) Source # |
|
(Monad m, Coercible newtyped (e_ (DepT e_ m))) => LiftDep (DepT e_ m) (ReaderT newtyped m) Source # |
This can be useful to "deceive" a function into using an environment possessing different instances than the instances seen by the function's dependencies. |
runDepT :: DepT e_ m r -> e_ (DepT e_ m) -> m r Source #
Runs a DepT
action in an environment.
>>>
runDepT (pure "foo") NilEnv
"foo"
For more sophisticated invocation functions, see runFinalDepT
and runFromEnv
from dep-t-advice.
:: forall small big m a. Monad m | |
=> (forall p q. (forall x. p x -> q x) -> small p -> small q) | rank-2 map function |
-> (forall t. big t -> small t) | get a small environment from a big one |
-> DepT small m a | |
-> DepT big m a |
Analogous to withReaderT
.
Changes the environment of a DepT
, for example making the DepT
work in
a "bigger" environment than the one in which was defined initially.
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.
:: forall small big m a. Monad m | |
=> (forall p q. (forall x. p x -> q x) -> small p -> small q) | rank-2 map function |
-> (forall t. big t -> small t) | get a small environment from a big one |
-> small (DepT small m) | |
-> small (DepT big m) |
Makes the functions inside a small environment require a bigger environment.
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.
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 } :}
However, this is only needed when the monad of the smaller environment
is already "fixed" before inserting it in the bigger one—which I expect
to be an infrequent case. When the concrete monad is selected after nesting
the environments, zoomEnv
shouldn't be necessary.
The simplest environment
An empty environment that carries no functions, analogous to ()
for ReaderT
.
The next simplest environment
Constant
, which has a phantom type parameter, is a valid environment for
DepT
.
DepT (Constant e) m
makes DepT
behave similarly to ReaderT e m
,
in that the environment e
is independent of the monad.
Constant functor.
Constant | |
|
Instances
Re-exports
module Control.Monad.Trans
module Control.Monad.Dep.Class
module Control.Monad.Reader.Class