Как вводить несколько зависимостей, когда я использую "монаду-читателю" для инъекции зависимостей?

Я пытаюсь использовать Reader monad для инъекции зависимостей, но имею проблемы, когда методы требуют разных зависимостей:

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

Вы можете видеть, что PageFetcher.fetch и ImageExtractor.extractImages и MyImageFinder.find имеют разные зависимости.

Я не уверен, правильно ли я использую Reader, и вскоре, когда я объединю их и хочу передать зависимости, я не знаю, как это сделать:

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

Обратите внимание на код MyImageFinder.find(url)(???), я хочу передать зависимости типа pageFetcher/imageExtractor/dep1/dep2/dep3, и как бы я ни старался, он просто не может быть скомпилирован.

Правильно ли я использую Reader? Как легко передать зависимости?

Ответ 1

Если вы хотите использовать несколько читателей в for -компостировании, типы аргументов должны быть одинаковыми, так или иначе. Один простой способ - просто связать все по типу среды (это может быть только кортеж), а затем использовать это как зависимость для всех ваших читателей.

Это отбрасывает много информации о мелкозернистых зависимостях в типах, но вы также можете использовать local как вид карты над входом в for -понимание:

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)

Так же, как map с функцией A => B может изменить a Reader[E, A] на a Reader[E, B], local с E => F изменяет Reader[F, A] на Reader[E, A], в этом случае беря конкретный кусок окружающая среда, в которой нуждается читатель, и кормить ее самостоятельно.

Обратите внимание, что на Kleisli есть много других комбинаторов (более общий тип - Reader - это просто псевдоним для Kleisli[Id, _, _]), который стоит прочитать.

Ответ 2

Обновление: удалена пользовательская плоская карта в пользу сказуса Reader

Как уже указывал Тревис, для использования шаблона Reader вам нужны одиночные функции аргументов. Поэтому, чтобы использовать его для нескольких зависимостей, вам нужно как-то связать все ваши зависимости в один аргумент. И здесь это становится интересным. Способ, показанный Travis, - это самый простой способ сделать это, но вам также нужно вручную переключать среды с помощью вызовов .local, и если вам нужны несколько зависимостей для поддеревьев вашего вычисления, вам необходимо вручную создать локальные среды.

Другим способом сделать это, чтобы подтипирование Scala показало это автоматически. До тех пор, пока ваши зависимости могут быть смешаны, составные вещи с разными или несколькими зависимостями работают (если вы действительно используете скалярный считыватель, а не если вы используете flatMap on Function1, как это делают некоторые из примеров Reader).

Вариант 1: образец пирожного чашки

Один из способов, позволяющий вашим зависимостям быть в состоянии смешать, - это урезанный шаблон торта. Я бы назвал его чашечным тортным рисунком, если бы мне пришлось назвать его именем, Dick Wall называет его Parfait (см. https://parleys.com/play/53a7d2cde4b0543940d9e55f/chapter28/about). Идея заключается не в том, чтобы помещать все в торт, а только помещать зависимости в торт и передавать его как объект контекста, который вы можете абстрагироваться с помощью читателя. Позвольте применить его к вашему примеру:

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

Образ чашечного торта становится сложным, если у вас есть несколько экземпляров одинаковых зависимостей (несколько журналов, несколько dbs и т.д.) и есть код, который вы хотите выборочно использовать на том или ином.

Вариант 2: карта с индексированием по типу

Недавно я придумал другой способ сделать это, используя специальную структуру данных, которую я называю картографией с индексированием типов. Он сохраняет всю плиту котла с чашечным котлом, и это упрощает использование нескольких экземпляров одного и того же типа зависимости (т.е. Просто обернуть их в отдельные классы-члены, чтобы отличить их).

/** 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")
  }
}

Я опубликовал его здесь https://github.com/cvogt/slick-action/. Соответствующие тестовые примеры приведены здесь: https://github.com/cvogt/slick-action/blob/master/src/test/scala/org/cvogt/di/TMapTest.scala#L213 Это на maven, но будьте осторожны при его использовании, потому что код находится в потоке и текущая реализация не является потокобезопасным в 2.10, только в 2.11, потому что он полагается на TypeTags. Я, вероятно, опубликую версию, которая работает в течение 2.10 и 2.11 в какой-то момент.

Добавление Хотя это решает многозависимую инъекцию с монада-читателя, вы все равно получите ошибки типа для htmlTry, потому что вы смешиваете композицию Reader/Function1 с Try-composition. Решение состоит в создании оберточной Monad, которая внутренне обертывает Function1 [TMap [...], Try [...]] и позволяет создавать их. Это требует, чтобы вы вносили все в этот тип монады, даже если что-то не понадобилось бы. Попробуйте.