14

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?

Freewind
  • 193,756
  • 157
  • 432
  • 708
  • first of all, if you want to pass implicit defined value, you need to define the method to accept implicit value first. – user1484819 Sep 20 '14 at 12:20
  • Sorry, the `implicit` are copied by mistake, they should not be there in this example. Thanks – Freewind Sep 20 '14 at 12:34
  • Kind of a related question on how Reader Monad DI compares to constructor-params DI, and how to use RM with multiple dependencies/nested method calls: http://stackoverflow.com/questions/29174500/reader-monad-for-dependency-injection-multiple-dependencies-nested-calls – adamw Mar 20 '15 at 19:37

2 Answers2

13

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 the for-comprehension:

case class Foo(i: Int)
case class Bar(s: String)
case class Config(foo: Foo, bar: Bar)

val doSomethingWithFoo: Reader[Foo, String] = Reader(foo => "hello " * foo.i)
val doSomethingWithBar: Reader[Bar, String] = Reader(bar => s"bar is $bar")

val doSomethingWithConfig: Reader[Config, String] = for {
  resFoo <- doSomethingWithFoo.local(_.foo)
  resBar <- doSomethingWithBar.local(_.bar)
} yield (resFoo, resBar)

Just as map with a function A => B can change a Reader[E, A] to a Reader[E, B], local with E => F changes Reader[F, A] to Reader[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 for Kleisli[Id, _, _]) that are worth reading up on.

Travis Brown
  • 138,631
  • 12
  • 375
  • 680
  • While this is how it can be done in Haskell and using scalaz, Scala's subtyping opens another intriguing possibility, which I describe in my answer. – cvogt Sep 24 '14 at 16:52
  • @TravisBrown I think you wanted to mean: **local with `E => F` changes `Reader[E, A]` to `Reader[F, A]`** instead of **local with `E => F` changes `Reader[F, A]` to `Reader[E, A]`**. Great answer by the way! – Mik378 Apr 10 '17 at 14:01
  • 1
    @Mik378 Thanks, but it actually is the way I wrote it, since `local` is mapping over the inputs, not the outputs. In this example specifically, `_.foo` is a `Config => Foo`, and calling `.local(_.foo)` on a `Reader[Foo, X]` gives a `Reader[Config, X]`. – Travis Brown Apr 10 '17 at 14:07
  • 1
    Indeed, you're completely right. I also thought while mapping over the inputs but I reversed the types E and F. Thanks for your reactivity. – Mik378 Apr 10 '17 at 14:36
7

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:

// business logic
class PageFetcher {
  def fetch(url: String) = Reader((deps: Dep1Component) => Try {
    ...
  })
}

class ImageExtractor {
  def extractImages(html: String) = Reader((deps: (Dep2Component with Dep3Component)) => {
    ...
  })
}


object MyImageFinder {
  def find(url: String) = 
    for {
      pageFetcher <- Reader((deps: PageFetcherComponent) => dep.pageFetcher)
      imageExtractor <- Reader((deps: ImageExtractorComponent) => dep.imageExtractor)
      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

// cupcake modules
trait PageFetcherComponent{
  def pageFetcher: PageFetcher
}
trait ImageExtractorComponent{
  def imageExtractor: ImageExtractor
}
trait Dep1Component{
  def dep1: Dep1
}
trait Dep2Component {
  def dep2: Dep2
}
trait Dep3Component{
  def dep3: Dep3
}

object Dependencies extends PageFetcherComponent with ImageExtractorComponent with Dep1Component with Dep2Component with Dep3Component{
  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)(Dependencies) match {
        case Success(images) => images.foreach(println)
        case Failure(err) => println(err.toString)
      }
    case _ => println("Please input an url")
  }
}

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).

/** gets stuff out of a TMap */
def Implicit[V:TTKey] = Reader((c: TMap[V]) => c[V])

// business logic
class PageFetcher {
  def fetch(url: String) = Implicit[Dep1].map{ dep1 => Try {
    ...
  }}
}

class ImageExtractor {
  def extractImages(html: String) = for{
    dep2 <- Implicit[Dep1]
    dep3 <- Implicit[Dep3]
  } yield {
    ...
  }
}


object MyImageFinder {
  def find(url: String) = 
    for {
      pageFetcher <- Implicit[PageFetcherComponent]
      imageExtractor <- Implicit[ImageExtractorComponent]
      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

val Dependencies =
  TMap(new PageFetcher) ++
  TMap(new ImageExtractor) ++
  TMap(new Dep1) ++
  TMap(new Dep2) ++
  TMap(new Dep3)

def main(args: Array[String]) {
  args.headOption match {
    case Some(url) =>
      MyImageFinder.find(url)(Dependencies) match {
        case Success(images) => images.foreach(println)
        case Failure(err) => println(err.toString)
      }
    case _ => println("Please input an url")
  }
}

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.

Blaisorblade
  • 6,438
  • 1
  • 43
  • 76
cvogt
  • 11,260
  • 30
  • 46
  • This is an amazing answer. Really well done. cup cake pattern is basically what today as of 2020 Zio call the module pattern. Well done. – MaatDeamon Dec 13 '20 at 23:46