5 ms·
The reader monad removes 90% of the syntactic overhead of dependency passing. That's the point. If you want dependency injection as you've defined it, you can
by wyager 10y ago
The reader monad removes 90% of the syntactic overhead of dependency passing. That's the point.
If you want dependency injection as you've defined it, you can use (if we're talking about Haskell) typeclasses or, by extension, implicit parameters, to do dependency injection in the way you like.
It's still much safer and easier to reason about than Java-style dynamic dependency injection.
- hota_mazi 10y agoActually, `Reader` adds a lot of boiler plate that's not present with traditional @Inject injection: - 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.
- thinkpad20 10y agoThere is a very small amount of boilerplate. And I would argue strongly that it's a good thing; it indicates to the reader of the code that an object's behavior reads from some initialized value, or equivalently that its behavior depends on some initial value which remains fixed through the computation. The reader monad gives you a simple language to express this common pattern, as well as the ability to easily set the behavior. -- 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.
- hota_mazi 10y agoWhat if `function2` needs to log something? Without dependency injection, you need to pass that logger to the function. With dependency injection, that logger is available without having to pollute the method signature with an implementation detail.
- thinkpad20 10y agoIf `function2` needs to log something, then it should have a type signature which reflects that. 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.
- hota_mazi 10y ago> If `function2` needs to log something, then it should have a type signature which reflects that. 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?
- thinkpad20 10y ago> The fact that `function2` is logging stuff is an implementation detail that callers shouldn't care about. 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...
- fmap 10y agoYou might be interested in reflection/implicit configurations: https://hackage.haskell.org/package/reflection https://hackage.haskell.org/package/reflection This (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/implicits.pdf 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).