Haskell - how to combine components that have different subsets of dependencies?
How can I integrate an application of components that require different subsets of the infrastructure?
Some of them will be very simple and require only a configuration reader (I only want to expose the appropriate subset for each business function) and possibly a logger. Some will also need to connect to external services (with a cache) where I also want to expose the limited amount of possible interactions with the outside world.
I don't want to deal with passing multiple arguments to such functions explicitly or wrapping them in some MonadIO
that can do everything.
What would be the closest thing to multiple injection like in Java application containers?
source to share
The library mtl there are classes of classes to represent computations that read the configuration of the environment, MonadReader
and record the data in something like a registrar MonadWriter
. We will use them for our examples.
The part MonadReader
we will be using will be
class Monad m => MonadReader r m | m -> r where
ask :: m r
The part MonadWriter
we will be using will be
class (Monoid w, Monad m) => MonadWriter w m | m -> w where Source
tell :: w -> m ()
To require " multiple dependencies ", we need one m
that provides instances for multiple typeclasses.
Boilerplate
We will end up using ReaderT
both and WriterT
and Identity
from transformers to run our example.
{-# LANGUAGE FlexibleContexts #-}
--mtl
import Control.Monad.Reader.Class
import Control.Monad.Writer.Class
--transformers
import Control.Monad.Trans.Reader hiding (ask)
import Control.Monad.Trans.Writer.Strict hiding (tell)
import Data.Functor.Identity
Using multiple dependencies: reading configuration and logging
Our reader will read next Wednesday; he will provide MonadReader Configuration
.
data Configuration = Config { site :: String }
deriving Show
Our recorder will accumulate a list of messages, each of which is a string. He will provide MonadWriter [String]
.
You can require multiple capabilities by requiring instances of multiple typeclasses. To do this, you may need one m
that has instances for MonadReader
and MonadWriter
. Here is the component that requires an environment to read the configuration and a way to write log messages.
logConfig :: (MonadWriter String m, MonadReader Configuration m) => m ()
logConfig = do
config <- ask
tell [show config]
Providing Multiple Dependencies with Transformers
We can provide the required m
without touching IO
. ReaderT
from transformers adds the ability to read from medium to Monad
; we will use this to provide MonadReader Configuration
. WriterT
from transformers adds that the ability to store output in Monad
; we will use this to provide MonadWriter [String]
. For the basic one Monad
we just use Identity
to show that we are not messing with IO
. Below are both MonadReader Configuration
, and MonadWriter [String]
, and the computation is performed without using IO
.
type DepsIdentity = ReaderT Configuration (WriterT [String] Identity)
runDepsIdentity :: DepsIdentity a -> Configuration -> (a, [String])
runDepsIdentity ma = runIdentity . runWriterT . runReaderT ma
Running the example
We'll use our previous one logConfig
in another, larger example:
example :: (MonadWriter [String] m, MonadReader Configuration m) => m ()
example = do
tell ["Starting", "Logging Config"]
logConfig
tell ["Done Logging Config", "Done"]
Finally, we'll run example
with help Configuration
and see what it does. Note that IO
this is only used to output the final output to run the example.
main :: IO ()
main = print . runDepsIdentity example $ Config {site = "StackOverflow"}
This creates the following output
((),["Starting","Logging Config","Config {site = \"StackOverflow\"}","Done Logging Config","Done"])
source to share