简体   繁体   中英

Composition of Readers for Dependency Injection in Scala

Here is a simple service example, whose methods return reader:

trait Service1_1{
  def s1f1:Reader[Map[String,Int],Int] =
    Reader(_("name"))

  def s1f2:Reader[Map[String,Int],Int] =
    Reader(_("age"))
}

Here is a service-consumer, that accepts parameter, map, and also returns reader itself:

trait Service1_2 {
  def s12f1(i:Int, map:Map[String,Int]):Reader[Service1_1, Int] =
    Reader(s => {
      val r = for {
        r1 <- s.s1f1
        r2 <- s.s1f2
      } yield r1 + r2
      r.run(map) + i
    })
}

Ok, to use Service1_2.s12f1 I must have map in the parameter list:

object s1 extends Service1_1
object s2 extends Service1_2
val r = s2.s12f1(3, Map("age"-> 1, "name"-> 2)).run(s1)

The question: how to implement Service1_2.s12f2 :

trait Service1_2 {
  def s2f2 = ???
}

In order to be able to run it like:

s2.s2f2(2)
  .run(s1)
  .run(Map("age"-> 1, "name"-> 2))

The main idea is to postpone passing of dependency to the execution. This should allow to get a better composition and deferred execution. How to get it work? What are the best practices with Readers, in case there are nested calls with such dependencies. For instance, imagine service, Service1_3 , which in one method, will use both Service1_2.s2f2 and Service1_1.s1f1

UPDATE , ok, I could implement it, but it looks, over complicated:

  def s2f2(i:Int): Reader[Service1_1, Reader[Map[String,Int],Int]] =
    Reader(s => Reader(map => {
      val r = for {
        r1 <- s.s1f1
        r2 <- s.s1f2
      } yield r1 + r2
      r.run(map) + i
    }))

The question, is there a better approach? Or at least syntax? Cause with several levels of dependency, it will look strange.

I'd probably "uncurry" the reader, so that instead of having two (or more) layers of readers, I'd have an n-tuple as the environment. Then you can "raise" smaller readers into the current level with local .

For example, instead of Reader[Service1_1, Reader[Map[String, Int], Int]] I'd use Reader[(Service1_1, Map[String, Int]), Int] :

import cats.data.Reader

trait Service1_1{
  def s1f1: Reader[Map[String, Int], Int] = Reader(_("name"))
  def s1f2: Reader[Map[String, Int], Int] = Reader(_("age"))
}

trait Service1_2 {
  type Env = (Service1_1, Map[String,Int])

  def s2f2(i: Int): Reader[Env, Int] =
    for {
      s <- Reader((_: Env)._1)
      r1 <- s.s1f1.local((_: Env)._2)
      r2 <- s.s1f2.local((_: Env)._2)
    } yield r1 + r2 + i
}

And then:

scala> object s1 extends Service1_1
defined object s1

scala> object s2 extends Service1_2
defined object s2

scala> s2.s2f2(2).run((s1, Map("age"-> 1, "name"-> 2)))
res0: cats.Id[Int] = 5

This works exactly the same as your s2f2 except that instead of s2.s2f2(2).run(s1).run(myMap) we write s2.s2f2(2).run((s1, myMap)) , or even just s2.s2f2(2).run(s1, myMap) using adapted args.

The advantage of this approach is that even as you add layers, you can compose the new and previous readers in a single for -comprehension via local .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM