Як змоделювати цей приклад
Як це можна було змоделювати за допомогою монади Reader?
Я не впевнений, чи слід це моделювати за допомогою 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
методу не потрібно знати про залежність Datastore
Крок моделювання 2. Використання Reader для складання функцій та їх запуску
Reader monad дозволяє створювати лише функції, які залежать від одного типу. Це часто не так. У нашому прикладі
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 / с допомогою кортежів для цього. Читач змушує вас додати якийсь код, який не стосується проблемного домену, отже, вносячи певний шум у код. З іншого боку, програма, яка використовує конструктори, часто використовує заводський шаблон, який також виходить за межі проблемного домену, тому ця слабкість не така вже й серйозна.
Що робити, якщо я не хочу перетворювати свої класи на об’єкти з функціями?
Ти хочеш. Ви технічно можете цього уникнути, але просто подивіться, що сталося б, якби я не перетворив FindUsers
клас на об'єкт. Відповідний рядок для розуміння буде виглядати так:
getAddresses <- ((ds: Datastore) => new FindUsers(ds).inactive _).local[Config](_.dataStore)
що не так читабельно, чи не так? Справа в тому, що Reader оперує функціями, тому якщо у вас їх ще немає, вам потрібно побудувати їх вбудовано, що часто не так красиво.