Состав читателей для внедрения зависимостей в Scala

Вот пример простого сервиса, методы которого возвращают читатель:

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

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

Вот сервис-потребитель, который принимает параметр, карту, а также возвращает сам читатель:

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

Хорошо, чтобы использовать Service1_2.s12f1, у меня должна быть карта в списке параметров:

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

Вопрос: как реализовать Service1_2.s12f2:

trait Service1_2 {
  def s2f2 = ???
}

Чтобы иметь возможность запускать его так:

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

Основная идея — отложить передачу зависимости до исполнения. Это должно позволить получить лучшую композицию и отложенное исполнение. Как заставить его работать? Каковы наилучшие методы работы с Readers, если есть вложенные вызовы с такими зависимостями. Например, представьте сервис Service1_3, который в одном методе будет использовать как Service1_2.s2f2, так и Service1_1.s1f1.

ОБНОВЛЕНИЕ, хорошо, я мог бы это реализовать, но это выглядит слишком сложно:

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

Вопрос, есть ли лучший подход? Или хотя бы синтаксис? Потому что с несколькими уровнями зависимости это будет выглядеть странно.


person Alexandr    schedule 13.03.2019    source источник


Ответы (1)


Я бы, вероятно, «отменил» считыватель, чтобы вместо двух (или более) слоев считывателей у меня был n-кортеж в качестве среды. Затем вы можете «поднять» меньших читателей на текущий уровень с помощью local.

Например, вместо Reader[Service1_1, Reader[Map[String, Int], Int]] я бы использовал 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
}

А потом:

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

Это работает точно так же, как ваш s2f2, за исключением того, что вместо s2.s2f2(2).run(s1).run(myMap) мы пишем s2.s2f2(2).run((s1, myMap)) или даже просто s2.s2f2(2).run(s1, myMap), используя адаптированные аргументы.

Преимущество этого подхода в том, что даже когда вы добавляете слои, вы можете скомпоновать новых и предыдущих читателей в единое for-понимание через local.

person Travis Brown    schedule 13.03.2019
comment
Спасибо за твой ответ. Интересный момент. Не могли бы вы немного объяснить, что делает «local» в выражении: «s.s1f1.local((_: Env)._2)»? - person Alexandr; 13.03.2019
comment
@Alexandr Это все равно, что поставить операцию перед функцией чтения. Это позволяет вам расширить, например. от Reader[A, B] до Reader[(A, C), B]. - person Travis Brown; 13.03.2019
comment
спасибо! Это действительно очень полезно знать. Можете порекомендовать что-нибудь дополнительно почитать, где описаны такие проблемы. Большинство статей о ридерах просто показывают базовый случай, который настолько прост, но на практике его нельзя использовать, не зная других возможностей. - person Alexandr; 13.03.2019
comment
Я согласен — в целом кажется, что есть много вводных сообщений в блогах по темам, связанным с кошками, которые не очень хорошо представляют свои темы. Краткий обзор этого кажется немного лучше, чем большинство? - person Travis Brown; 13.03.2019
comment
Также есть документация Kleisli: typelevel.org/cats/datatypes/kleisli.html - person Travis Brown; 13.03.2019