Ответ 1
Как моделировать этот пример
Как это можно смоделировать с помощью монады Reader?
Я не уверен, что это должно быть смоделировано с помощью Reader, но это может быть:
- кодирование классов как функций, которые делают код более приятным с Reader
- составление функций с помощью Reader в для понимания и использования
Как раз перед стартом мне нужно рассказать вам о небольших корректировках кода образца, которые я счел полезными для этого ответа.
Первое изменение касается метода FindUsers.inactive
. Я разрешаю ему возвращать List[String]
, поэтому можно использовать список адресов
в методе UserReminder.emailInactive
. Я также добавил простые реализации методов. Наконец, образец будет использовать
следующая ручная версия монады читателя:
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 = ???
}
// usage: val result = new Foo(dependency).bar(arg)
становится
object Foo {
def bar: Dep => Arg => Res = ???
}
// usage: val result = Foo.bar(dependency)(arg)
Имейте в виду, что каждый из типов 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()
}
}
Здесь следует отметить, что определенные функции не зависят от всех объектов, а только от непосредственно используемых частей.
Где в версии OOP UserReminder.emailInactive()
экземпляр вызовет userFinder.inactive()
здесь, он просто вызывает inactive()
- функция, переданная ему в первом параметре.
Обратите внимание, что код показывает три желательных свойства вопроса:
- ясно, какие зависимости нужны каждой функциональности
- скрывает зависимости одной функциональности от другой
-
retainUsers
метод не должен знать о зависимости Datastore
Шаг моделирования 2. Используя 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("[email protected]") },
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 для такого "бизнес-приложения" лучше, чем просто использование параметров конструктора?
Надеюсь, что, подготовив этот ответ, я упростил судить о себе, в каких аспектах он победит простых конструкторов. Но если бы я перечислил их, вот мой список. Отказ от ответственности: у меня есть фон ООП, и я, возможно, не ценю Читателя и Клейсли полностью, поскольку я их не использую.
- Однородность - не малый, насколько короткий/длительный для понимания, это просто читатель, и вы можете легко составить его с помощью другого
экземпляр, возможно, только введение еще одного типа Config и разбрызгивание некоторых
local
вызовов поверх него. Этот момент - ИМО скорее вопрос вкуса, потому что, когда вы используете конструкторы, никто не мешает вам сочинять все, что вам нравится, если кто-то делает что-то глупое, как работа в конструкторе, которая считается плохой практикой в ООП. - Reader - это монада, поэтому он получает все преимущества, связанные с этим -
sequence
,traverse
методы реализованы бесплатно. - В некоторых случаях предпочтительнее создавать Reader только один раз и использовать его для широкого спектра конфигураций. С конструкторами никто не мешает вам это сделать, вам просто нужно построить весь графический объект заново для каждой конфигурации входящие. Хотя у меня нет проблем с этим (я даже предпочитаю делать это при каждом запросе приложения), это не явная идея для многих людей по причинам, о которых я могу только догадываться.
- Читатель подталкивает вас к использованию функций больше, что будет лучше работать с приложением, написанным преимущественно в стиле FP.
- Читатель разделяет проблемы; вы можете создавать, взаимодействовать со всем, определять логику без предоставления зависимостей. Собственно поставляйте позже, отдельно. (Спасибо Кен Скрамблеру за этот момент). Это часто слышно преимущество Reader, но это также возможно с помощью простых конструкторов.
Я также хотел бы рассказать, что мне не нравится в Reader.
- Маркетинг. Иногда я получаю впечатление, что Reader продается для всех видов зависимостей, без различия, если это cookie сеанса или базу данных. Мне мало смысла в использовании Reader для практически постоянных объектов, таких как электронная почта сервера или репозитория из этого примера. Для таких зависимостей я нахожу простые конструкторы и/или частично прикладные функции лучше. По сути, Reader дает вам гибкость, поэтому вы можете указывать свои зависимости при каждом вызове, но если вы на самом деле это не нужно, вы платите только налог.
- Неявная тяжесть - использование Reader без implicits сделает пример трудным для чтения. С другой стороны, когда вы прячете шумные части с использованием implicits и некоторые ошибки, компилятор иногда может затруднить расшифровку сообщений.
- Церемония с
pure
,local
и создание собственных классов Config/использование кортежей для этого. Читатель заставляет вас добавить код это не о проблемной области, поэтому вводить некоторый шум в код. С другой стороны, приложение который использует конструкторы, часто использует шаблон factory, который также находится за пределами проблемной области, поэтому эта слабость не та, что серьезно.
Что делать, если я не хочу преобразовывать свои классы в объекты с функциями?
Вы хотите. Вы технически можете избегать этого, но просто посмотрите, что произойдет, если я не преобразовал класс FindUsers
в объект. Соответствующая строка для понимания будет выглядеть так:
getAddresses <- ((ds: Datastore) => new FindUsers(ds).inactive _).local[Config](_.dataStore)
который не является читаемым, является ли это? Дело в том, что Reader работает с функциями, поэтому, если у вас их уже нет, вам нужно построить их встроенные, что часто бывает не так.