Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to inject multi dependencies when I use "Reader monad" for dependency injection?

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?

like image 799
Freewind Avatar asked Sep 20 '14 10:09

Freewind


2 Answers

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.

like image 193
Travis Brown Avatar answered Nov 07 '22 05:11

Travis Brown


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.

like image 30
cvogt Avatar answered Nov 07 '22 06:11

cvogt