- All your functions now need to return a Reader[C,A] instead of just A
- You need to pass all the parameters explicitly in each method signature as opposed to passing just the ones that don't need to be injected.
- All your functions now need to return a Reader[C,A] instead of just A
- You need to pass all the parameters explicitly in each method signature as opposed to passing just the ones that don't need to be injected.
-- This function will always return the same thing given the same input
function1 :: Int -> String
-- This function depends on reading some configuration which
-- needs to be provided upstream
function2 :: Int -> Reader Configuration String
You don't need to pass parameters explicitly in each signature; indeed this is exactly what the reader monad obviates: the details of what is being read are not expressed inside the function (until the point that they they are actually used). This is hardly an onerous burden, in my opinion. And if typing `Reader X Y` is too annoying, you can just make a type alias.Logging is a side-effect. Logging requires configuration to be passed in; it means having access to some file descriptor or other object to interact with, it could potentially fail to connect, or cause a computation to hang, or cause a service to trigger, or make a disk run out of space, etc. If a function wants to log something it's not a simple reader anymore but something more complex. The fact that in Haskell this is reflected in the type signature of the function is again a good thing. It's not "polluting" the method signature; it's putting more information in the method signature. Not letting you hide side effects in a computation that appears to have no externalities is a strength of Haskell, not a weakness.
Yes if you value referential transparency.
No if you value encapsulation.
The fact that `function2` is logging stuff is an implementation detail that callers shouldn't care about. They should certainly not be forced to pass that function a logger.
What if that function decides that on top of logging, it wants to store stuff in a database. Should all callers suddenly find some kind of database to pass to that function too?
On the contrary, I think it definitely matters. If a function is going to log something, I want to know about it. Those logs could cause me problems (e.g. polluting my stdout or attempting to write to a file they don't have permissions on), or I might want to control where those logs go, what the log level is, what the format is, et cetera. This is absolutely something I want to know about.
> What if that function decides that on top of logging, it wants to store stuff in a database. Should all callers suddenly find some kind of database to pass to that function too?
Yes, a thousand times yes. Why would I want a function to be storing stuff in a database without my knowledge? If a function is going to write to a database, it's all the more important that the caller is aware of that. How can I access whatever it stores? How do I know what database it's writing to? How can I be sure that database is properly initialized and/or torn down? How do I know whether the function is threadsafe? How do I know it's a secure connection? Et cetera.
If you want to write a function which does "arbitrary side effects", easy: just write all of your code in the IO monad.
-- It reverses a string... and who knows what else!
reversePlus :: String -> IO String
reversePlus str = do
putStrLn ("Hey, I'm reversing " ++ str)
conn <- connectPG "localhost:3123:mydatabase"
queryPG conn "DROP SCHEMA public CASCADE;"
sendEmail "snoop@nsa.gov" "hey guys what's up"
return $ reverse str
Of course, I don't recommend this...> On the contrary, I think it definitely matters. If a function is going to log something, I want to know about it.
You are missing the forest for the trees.
First of all, why you'd care that a function you're calling is logging stuff is a bit beyond me but fine. Think of something else. Maybe the function is calling memcache, or storing stuff in the database, or sending a UDP packet to a message bus, or is querying the location service. Surely you can agree that there are things this function does that you don't care about if all you need is an Account given a user id, right?
These things you don't care about are called implementation details. Callers shouldn't know about them, therefore they shouldn't have to pass them in parameters.
That's what dependency injection (injection, not passing) does for you. It lets you call
val account = getAccount(userId)
instead of val account = getAccount(userId, logger, memCache, db, messageBus)
The first example is using dependency injection and correctly hides the implementation details of `getAccount` while not being referentially transparent.The second example is referentially transparent but exposes all kinds of private implementation details, making the callers' life very difficult, if not impossible (how are they supposed to come up with a messageBus when all they have is a user id?).
eg. what if getAccount was hard coded to initialize all the other dependencies it needed on the fly for each call?
If that's still considered DI then it's a much looser term than I understood it to be.
We are in strong disagreement about what constitutes an "implementation detail".
But also, you can just use a monad transformer stack and add whatever side-effectful operations you want into it, use it as needed. Boom, dependency injection. And more control over what your functions actually do is there when you need it.
class (MonadIO m) => HasLogging m where
log :: String -> m ()
data AppConfig = AppConfig { stuff :: Int }
newtype MyApp a = MyApp { runApp :: ReaderT AppConfig IO a}
deriving (Functor, Applicative, Monad, MonadIO, MonadReader AppConfig)
instance HasLogging MyApp where
log s = liftIO (putStrLn s)
function2 :: Int -> MyApp String
function2 x = do
log "hey guys I'm logging"
return (show x)
-- or without specifying the base monad, yay abstraction
function2' x = do
log "heyooo logging here"
return (show x)
-- Haskell will infer this type:
-- function2' :: (HasLogging m, Show a) => a -> m StringThis (ab)uses Haskell's type class mechanism to essentially implement dependency injection directly. The implementation looks a bit dirty, but this is a feature that more modern approaches to generic programming can handle natively (e.g., http://homepages.inf.ed.ac.uk/wadler/papers/implicits/implic... ).
In particular, there is nothing shady about the semantics of implicitly passing configuration values/dependencies. Your functions are still referentially transparent if you treat the implicit dependencies as additional parameters (which is what they are, no matter how you implement it).