Dependency management is a big problem in OOP for the following two reasons:
- The tight coupling of data and code.
- Ubiquitous use of side effects.
Most OO programmers consider the tight coupling of data and code to be wholly beneficial, but it comes with a cost. Managing the flow of data through the layers is an unavoidable part of programming in any paradigm. Coupling your data and code adds the additional problem that if you want to use a function at a certain point, you have to find a way get its object to that point.
Use of side effects creates similar difficulties. If you use a side effect for some functionality, but want to be able to swap out its implementation, you pretty much have no other choice but to inject that dependency.
Consider as an example a spammer program that scrapes web pages for email addresses then emails them. If you have a DI mindset, right now you're thinking of the services you will encapsulate behind interfaces, and which services will get injected where. I'll leave that design as an exercise for the reader. If you have an FP mindset, right now you're thinking of the inputs and outputs for the lowest layer of functions, like:
- Input a web page address, output the text of that page.
- Input a page's text, output a list of links from that page.
- Input a page's text, output a list of email addresses on that page.
- Input a list of email addresses, output a list of email addresses with duplicates removed.
- Input an email address, output a spam email for that address.
- Input a spam email, output the SMTP commands to send that email.
When you think in terms of inputs and outputs, there are no function dependencies, only data dependencies. That's what makes them so easy to unit test. Your next layer up arranges for the output of one function to be fed into the input of the next, and can easily swap out the various implementations as needed.
In a very real sense, functional programming naturally prods you to always invert your function dependencies, and therefore you usually don't have to take any special measures to do so after the fact. When you do, tools like higher-order functions, closures, and partial application make it easier to accomplish with less boilerplate.
Note that it's not dependencies themselves that are problematic. It's dependencies that point the wrong way. The next layer up may have a function like:
processText = spamToSMTP . emailAddressToSpam . removeEmailDups . textToEmailAddresses
It's perfectly okay for this layer to have dependencies hard-coded like this, because its sole purpose is to glue the lower-layer functions together. Swapping an implementation is as simple as creating a different composition:
processTextFancy = spamToSMTP . emailAddressToFancySpam . removeEmailDups . textToEmailAddresses
This easy recomposition is made possible by a lack of side effects. The lower-layer functions are completely independent of each other. The next layer up may choose which
processText is actually used based on some user config:
actuallyUsedProcessText = if (config == "Fancy") then processTextFancy else processText
Again, not an issue because all the dependencies point one way. We don't need to invert some dependencies in order to get them all pointing the same way, because pure functions already forced us to do so.
Note that you could make this a lot more coupled by passing
config down through to the lowest layer instead of checking it at the top. FP doesn't prevent you from doing this, but it does tend to make it a lot more annoying if you try.