I'm trying to use Reader monad
for dependency injection, but have problems when the methods requires different dependencies:
class PageFetcher {
def fetch(url: String) = Reader((dep1: Dep1) => Try {
...
})
}
class ImageExtractor {
def extractImages(html: String) = Reader((deps: (Dep2, Dep3)) => {
...
})
}
object MyImageFinder {
def find(url: String) = Reader((deps: (PageFetcher, ImageExtractor)) => {
val (pageFetcher, imageExtractor) = deps
for {
htmlTry <- pageFetcher.fetch(url)
html <- htmlTry
images <- imageExtractor.extractImages(html)
} yield images
})
}
// I add these 3 useless dependencies here just for demo
class Dep1
class Dep2
class Dep3
You can see PageFetcher.fetch
and ImageExtractor.extractImages
and MyImageFinder.find
all have different dependencies.
I'm not sure if the way I use the Reader
correctly, and soon when I combine them together and want to pass the dependencies, I don't know how to do it:
val pageFetcher = new PageFetcher
val imageExtractor = new ImageExtractor
val dep1 = new Dep1
val dep2 = new Dep2
val dep3 = new Dep3
def main(args: Array[String]) {
args.headOption match {
case Some(url) =>
MyImageFinder.find(url)(???) match {
case Success(images) => images.foreach(println)
case Failure(err) => println(err.toString)
}
case _ => println("Please input an url")
}
}
Notice the code MyImageFinder.find(url)(???)
, I want to pass the dependencies like pageFetcher/imageExtractor/dep1/dep2/dep3
, and no matter how I tried, it just can't be compiled.
Is my way to use Reader
correct? How can I pass the dependencies easily?
Update: removed custom flatMap in favor of scalaz's Reader
As Travis already pointed out, to use the Reader pattern, you need single argument functions. So in order to use it for multiple dependencies, you somehow need to get all of your dependencies into a single argument. And here it becomes interesting. The way Travis showed is the simplest way to do it, but you also have to manually switch environments using the
.local
calls and if you need multiple dependencies for subtrees of your computation, you need to manually build local environments.Another way to to it is to let Scala's subtyping figure it out auto-magically. As long as your dependencies can be mixed in, composing things with different or multiple dependencies just works (if you actually use scalaz's Reader, not if you use flatMap on Function1 as some of the Reader examples do).
Option 1: Cup cake pattern
One way to allow your dependencies to be able to mixed in is a stripped down cake pattern. I'd call it cup-cake pattern, if I had to give it a name, Dick Wall calls it Parfait (see https://parleys.com/play/53a7d2cde4b0543940d9e55f/chapter28/about ). The idea is instead of putting everything into the cake, only put the dependencies into the cake and pass it through as a context object, which you can abstract over using the reader. Let's apply it to your example:
The cup-cake pattern becomes tricky if you have multiple instances of the same dependencies (multiple loggers, multiple dbs, etc.) and have some code which you want to be able to selectively use on the one or the other.
Option 2: Type-indexed Map
I recently came up with another way to do it using a special data structure I call type-indexed map. It saves all the cup-cake boiler plate and it makes it much easier to use multiple instances of the same type of dependency (i.e. just wrap them in single member classes to distinguish them).
I published it here https://github.com/cvogt/slick-action/ . The corresponding test cases are here: https://github.com/cvogt/slick-action/blob/master/src/test/scala/org/cvogt/di/TMapTest.scala#L213 It's on maven, but be careful when using it, because the code is in flux and the current implementation is not thread-safe in 2.10, only in 2.11, because it relies on TypeTags. I'll probably publish a version that works for 2.10 and 2.11 at some point.
Addendum While this solves multi-dependency injection with the reader monad, you will still get type errors for htmlTry because you are mixing Reader/Function1-composition with Try-composition. The solution is to create a wrapping Monad that internally wraps Function1[TMap[...],Try[...]] and allow composing those. This does require you to stuff everything into this type of monad, even if something wouldn't need a Try.
If you want to use multiple readers in a
for
-comprehension, the argument types will need to be the same, one way or another. One easy way is just to bundle everything up in an environment type (it could just be a tuple), and then use that as the dependency for all your readers.That throws away a lot of information about fine-grained dependencies in the types, though, and you can also use
local
as a kind of map over the input in thefor
-comprehension:Just as
map
with a functionA => B
can change aReader[E, A]
to aReader[E, B]
,local
withE => F
changesReader[F, A]
toReader[E, A]
, in this case taking the specific chunk of the environment the reader needs and feeding it in by itself.Note that there are lots of other combinators on
Kleisli
(a more general type—Reader
is just an alias forKleisli[Id, _, _]
) that are worth reading up on.