When asked about Dependency Injection in Scala, quite a lot of answers point to the using the Reader Monad, either the one from Scalaz or just rolling your own. There are a number of very clear articles describing the basics of the approach (e.g. Runar's talk, Jason's blog), but I didn't manage to find a more complete example, and I fail to see the advantages of that approach over e.g. a more traditional "manual" DI (see the guide I wrote). Most probably I'm missing some important point, hence the question.
Just as an example, let's imagine we have these classes:
trait Datastore { def runQuery(query: String): List[String] }
trait EmailServer { def sendEmail(to: String, content: String): Unit }
class FindUsers(datastore: Datastore) {
def inactive(): Unit = ()
}
class UserReminder(findUser: FindUsers, emailServer: EmailServer) {
def emailInactive(): Unit = ()
}
class CustomerRelations(userReminder: UserReminder) {
def retainUsers(): Unit = {}
}
Here I'm modelling things using classes and constructor parameters, which plays very nicely with "traditional" DI approaches, however this design has a couple of good sides:
- each functionality has clearly enumerated dependencies. We kind of assume that the dependencies are really needed for the functionality to work properly
- the dependencies are hidden across functionalities, e.g. the
UserReminder
has no idea thatFindUsers
needs a datastore. The functionalities can be even in separate compile units - we are using only pure Scala; the implementations can leverage immutable classes, higher-order functions, the "business logic" methods can return values wrapped in the
IO
monad if we want to capture the effects etc.
How could this be modelled with the Reader monad? It would be good to retain the characteristics above, so that it is clear what kind of dependencies each functionality needs, and hide dependencies of one functionality from another. Note that using class
es is more of an implementation detail; maybe the "correct" solution using the Reader monad would use something else.
I did find a somewhat related question which suggests either:
- using a single environment object with all the dependencies
- using local environments
- "parfait" pattern
- type-indexed maps
However, apart from being (but that's subjective) a bit too complex as for such a simple thing, in all of these solutions e.g. the retainUsers
method (which calls emailInactive
, which calls inactive
to find the inactive users) would need to know about the Datastore
dependency, to be able to properly call the nested functions - or am I wrong?
In what aspects would using the Reader Monad for such a "business application" be better than just using constructor parameters?
How to model this example
I'm not sure if this should be modelled with the Reader, yet it can be by:
Just right before the start I need to tell you about small sample code adjustments that I felt beneficial for this answer. First change is about
FindUsers.inactive
method. I let it returnList[String]
so the list of addresses can be used inUserReminder.emailInactive
method. I've also added simple implementations to methods. Finally, the sample will use a following hand-rolled version of Reader monad:Modelling step 1. Encoding classes as functions
Maybe that's optional, I'm not sure, but later it makes the for comprehension look better. Note, that resulting function is curried. It also takes former constructor argument(s) as their first parameter (parameter list). That way
becomes
Keep in mind that each of
Dep
,Arg
,Res
types can be completely arbitrary: a tuple, a function or a simple type.Here's the sample code after the initial adjustments, transformed into functions:
One thing to notice here is that particular functions don't depend on the whole objects, but only on the directly used parts. Where in OOP version
UserReminder.emailInactive()
instance would calluserFinder.inactive()
here it just callsinactive()
- a function passed to it in the first parameter.Please note, that the code exhibits the three desirable properties from the question:
retainUsers
method should not need to know about the Datastore dependencyModelling step 2. Using the Reader to compose functions and run them
Reader monad lets you only compose functions that all depend on the same type. This is often not a case. In our example
FindUsers.inactive
depends onDatastore
andUserReminder.emailInactive
onEmailServer
. To solve that problem one could introduce a new type (often referred to as Config) that contains all of the dependencies, then change the functions so they all depend on it and only take from it the relevant data. That obviously is wrong from dependency management perspective because that way you make these functions also dependent on types that they shouldn't know about in the first place.Fortunately it turns out, that there exist a way to make the function work with
Config
even if it accepts only some part of it as a parameter. It's a method calledlocal
, defined in Reader. It needs to be provided with a way to extract the relevant part from theConfig
.This knowledge applied to the example at hand would look like that:
Advantages over using constructor parameters
I hope that by preparing this answer I made it easier to judge for yourself in what aspects would it beat plain constructors. Yet if I were to enumerate these, here's my list. Disclaimer: I have OOP background and I may not appreciate Reader and Kleisli fully as I don't use them.
local
calls on top of it. This point is IMO rather a matter of taste, because when you use constructors nobody prevents you to compose whatever things you like, unless someone does something stupid, like doing work in constructor which is considered a bad practice in OOP.sequence
,traverse
methods implemented for free.I would also like to tell what I don't like in Reader.
pure
,local
and creating own Config classes / using tuples for that. Reader forces you to add some code that isn't about problem domain, therefore introducing some noise in the code. On the other hand, an application that uses constructors often uses factory pattern, which is also from outside of problem domain, so this weakness isn't that serious.What if I don't want to convert my classes to objects with functions?
You want. You technically can avoid that, but just look what would happen if I didn't convert
FindUsers
class to object. The respective line of for comprehension would look like:which is not that readable, is that? The point is that Reader operates on functions, so if you don't have them already, you need to construct them inline, which often isn't that pretty.
I think the main difference is that in your example you are injecting all dependencies when objects are instantiated. The Reader monad basically builds a more and more complex functions to call given the dependencies, wich are then returned to the highest layers. In this case, the injection happens when the function is finally called.
One immediate advantage is flexibility, especially if you can construct your monad once and then want to use it with different injected dependencies. One disadvantage is, as you say, potentially less clarity. In both cases, the intermediate layer only need to know about their immediate dependencies, so they both work as advertised for DI.