Как смоделировать этот пример
Как это можно было смоделировать с помощью монады Читателя?
Я не уверен, следует ли это моделировать с помощью Reader, но это можно сделать:
- кодирование классов как функций, что улучшает работу кода с Reader
- составление функций с помощью Reader для понимания и использования
Непосредственно перед началом мне нужно рассказать вам о небольших корректировках кода, которые я посчитал полезными для этого ответа. Первое изменение касается FindUsers.inactive
метода. Я позволил ему вернуться, List[String]
чтобы можно было использовать список адресов в UserReminder.emailInactive
методе. Я также добавил простые реализации в методы. Наконец, в примере будет использоваться следующая свернутая вручную версия монады Reader:
case class Reader[Conf, T](read: Conf => T) { self =>
def map[U](convert: T => U): Reader[Conf, U] =
Reader(self.read andThen convert)
def flatMap[V](toReader: T => Reader[Conf, V]): Reader[Conf, V] =
Reader[Conf, V](conf => toReader(self.read(conf)).read(conf))
def local[BiggerConf](extractFrom: BiggerConf => Conf): Reader[BiggerConf, T] =
Reader[BiggerConf, T](extractFrom andThen self.read)
}
object Reader {
def pure[C, A](a: A): Reader[C, A] =
Reader(_ => a)
implicit def funToReader[Conf, A](read: Conf => A): Reader[Conf, A] =
Reader(read)
}
Шаг моделирования 1. Кодирование классов как функций
Возможно, это необязательно, я не уверен, но позже это улучшит понимание. Обратите внимание, что результирующая функция каррирована. Он также принимает аргументы бывшего конструктора в качестве первого параметра (списка параметров). Туда
class Foo(dep: Dep) {
def bar(arg: Arg): Res = ???
}
становится
object Foo {
def bar: Dep => Arg => Res = ???
}
Имейте в виду , что каждый из Dep
, Arg
,Res
типы могут быть совершенно произвольным: кортеж, функция или простой тип.
Вот пример кода после первоначальной настройки, преобразованный в функции:
trait Datastore { def runQuery(query: String): List[String] }
trait EmailServer { def sendEmail(to: String, content: String): Unit }
object FindUsers {
def inactive: Datastore => () => List[String] =
dataStore => () => dataStore.runQuery("select inactive")
}
object UserReminder {
def emailInactive(inactive: () => List[String]): EmailServer => () => Unit =
emailServer => () => inactive().foreach(emailServer.sendEmail(_, "We miss you"))
}
object CustomerRelations {
def retainUsers(emailInactive: () => Unit): () => Unit =
() => {
println("emailing inactive users")
emailInactive()
}
}
Здесь следует отметить, что отдельные функции зависят не от целых объектов, а только от непосредственно используемых частей. Если в версии ООП UserReminder.emailInactive()
экземпляр будет вызывать userFinder.inactive()
здесь, он просто вызывает inactive()
- функцию, переданную ему в первом параметре.
Обратите внимание, что код демонстрирует три желаемых свойства из вопроса:
- понятно, какие зависимости нужны каждой функциональности
- скрывает зависимости одной функциональности от другой
retainUsers
методу не нужно знать о зависимости хранилища данных
Шаг моделирования 2. Использование Reader для создания функций и их выполнения
Монада Reader позволяет создавать только функции, которые зависят от одного типа. Часто это не так. В нашем примере
FindUsers.inactive
зависит от Datastore
и UserReminder.emailInactive
отEmailServer
. Чтобы решить эту проблему, можно ввести новый тип (часто называемый Config), который содержит все зависимости, а затем изменить функции, чтобы все они зависели от него и брали от него только соответствующие данные. Это, очевидно, неверно с точки зрения управления зависимостями, потому что таким образом вы делаете эти функции зависимыми от типов, о которых они не должны знать в первую очередь.
К счастью, оказывается, что существует способ заставить функцию работать, Config
даже если она принимает в качестве параметра только часть ее. Это вызываемый метод local
, определенный в Reader. Необходимо предоставить способ извлечения соответствующей части из файла Config
.
Эти знания, примененные к рассматриваемому примеру, будут выглядеть так:
object Main extends App {
case class Config(dataStore: Datastore, emailServer: EmailServer)
val config = Config(
new Datastore { def runQuery(query: String) = List("john.doe@fizzbuzz.com") },
new EmailServer { def sendEmail(to: String, content: String) = println(s"sending [$content] to $to") }
)
import Reader._
val reader = for {
getAddresses <- FindUsers.inactive.local[Config](_.dataStore)
emailInactive <- UserReminder.emailInactive(getAddresses).local[Config](_.emailServer)
retainUsers <- pure(CustomerRelations.retainUsers(emailInactive))
} yield retainUsers
reader.read(config)()
}
Преимущества перед использованием параметров конструктора
В каких аспектах использование Reader Monad для такого «бизнес-приложения» было бы лучше, чем просто использование параметров конструктора?
Я надеюсь, что, подготовив этот ответ, мне стало легче самому судить, в каких аспектах он превзойдет простые конструкторы. Но если бы я перечислил их, вот мой список. Отказ от ответственности: у меня есть опыт работы с ООП, и я могу не оценить Reader и Kleisli в полной мере, поскольку я их не использую.
- Единообразие - независимо от того, насколько коротким / длинным является понимание, это просто Reader, и вы можете легко скомпоновать его с другим экземпляром, возможно, только добавив еще один тип Config и добавив несколько
local
вызовов поверх него. Это вопрос IMO, скорее, вопрос вкуса, потому что, когда вы используете конструкторы, никто не мешает вам создавать все, что вам нравится, если только кто-то не делает что-то глупое, например, выполнение работы в конструкторе, что считается плохой практикой в ООП.
- Читатель монада, поэтому он получает все преимущества , связанные с что -
sequence
, traverse
методы реализованы бесплатно.
- В некоторых случаях может оказаться предпочтительным собрать Reader только один раз и использовать его для широкого диапазона конфигураций. С конструкторами никто не мешает вам это делать, вам просто нужно заново строить весь граф объектов для каждого входящего Config. Хотя у меня нет проблем с этим (я даже предпочитаю делать это при каждом запросе к приложению), для многих это не очевидная идея по причинам, о которых я могу только догадываться.
- Reader подталкивает вас к большему использованию функций, которые будут лучше работать с приложениями, написанными преимущественно в стиле FP.
- Читатель разделяет проблемы; вы можете создавать, взаимодействовать со всем, определять логику без предоставления зависимостей. Собственно поставлю позже, отдельно. (Спасибо Кену Скремблеру за этот момент). Это часто называют преимуществом Reader, но это также возможно с простыми конструкторами.
Еще хочу сказать, что мне не нравится в Reader.
- Маркетинг. Иногда у меня складывается впечатление, что Reader продается для всех видов зависимостей, независимо от того, cookie это сеанса или база данных. Для меня нет смысла использовать Reader для практически постоянных объектов, таких как почтовый сервер или репозиторий из этого примера. Для таких зависимостей я считаю, что простые конструкторы и / или частично применяемые функции лучше. По сути, Reader дает вам гибкость, так что вы можете указывать свои зависимости при каждом вызове, но если вам это действительно не нужно, вы платите только налог.
- Неявная тяжесть - использование Reader без имплицитов затруднит чтение примера. С другой стороны, когда вы скрываете зашумленные части с помощью имплицитов и допускаете некоторую ошибку, компилятор иногда затрудняет расшифровку сообщений.
- Церемония с
pure
, local
и создание собственных классов Config / с помощью кортежей для этого. Reader вынуждает вас добавить код, не относящийся к проблемной области, что вносит некоторый шум в код. С другой стороны, приложение, использующее конструкторы, часто использует фабричный шаблон, который также находится за пределами проблемной области, поэтому эта слабость не так серьезна.
Что, если я не хочу преобразовывать свои классы в объекты с функциями?
Вы хотите. Технически этого можно избежать, но посмотрите, что бы произошло, если бы я не преобразовал FindUsers
класс в объект. Соответствующая строка для понимания будет выглядеть так:
getAddresses <- ((ds: Datastore) => new FindUsers(ds).inactive _).local[Config](_.dataStore)
который не читается, не так ли? Дело в том, что Reader работает с функциями, поэтому, если у вас их еще нет, вам нужно построить их встроенными, что часто не так уж и красиво.