如何为这个例子建模
如何使用 Reader monad 对其进行建模?
我不确定这个是否应该用 Reader 建模,但可以通过:
- 将类编码为函数,使代码在 Reader 中更好玩
- 用 Reader 在 for comprehension 中组合函数并使用它
就在开始之前,我需要告诉你一些我认为对这个答案有益的小示例代码调整。
第一个变化是关于FindUsers.inactive 方法。我让它返回List[String],这样地址列表就可以使用了
在UserReminder.emailInactive 方法中。我还为方法添加了简单的实现。最后,示例将使用
以下手动版本的 Reader monad:
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. 将类编码为函数
也许这是可选的,我不确定,但后来它使 for 理解看起来更好。
请注意,生成的函数是柯里化的。它还将以前的构造函数参数作为它们的第一个参数(参数列表)。
那样
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 编写函数并运行它们
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 在哪些方面比仅使用构造函数参数更好?
我希望通过准备这个答案,我可以更容易地自己判断它在哪些方面会击败普通的构造函数。
然而,如果我要列举这些,这是我的清单。免责声明:我有 OOP 背景,我可能不欣赏 Reader 和 Kleisli
完全因为我不使用它们。
- 一致性 - 无论 for 理解有多短/多长,它只是一个阅读器,您可以轻松地与另一个阅读器组合
例如,也许只引入了一种 Config 类型,并在其上添加了一些
local 调用。这一点是IMO
而是一个品味问题,因为当你使用构造函数时,没有人会阻止你编写任何你喜欢的东西,
除非有人做了一些愚蠢的事情,比如在构造函数中工作,这在 OOP 中被认为是一种不好的做法。
- Reader 是一个 monad,因此它可以获得与此相关的所有好处 - 免费实现
sequence、traverse 方法。
- 在某些情况下,您可能会发现最好只构建一次阅读器并将其用于各种配置。
使用构造函数没有人阻止你这样做,你只需要为每个 Config 重新构建整个对象图
传入。虽然我对此没有任何问题(我什至更喜欢在对应用程序的每个请求都这样做),但事实并非如此
对很多人来说,这是一个显而易见的想法,原因我只能推测。
- Reader 推动您更多地使用函数,这将在以 FP 风格为主的应用程序中发挥更好的作用。
- 阅读器分离关注点;您可以创建、与一切交互、定义逻辑而无需提供依赖项。实际供应以后,分开。 (感谢 Ken Scrambler 的这一点)。这通常是 Reader 的优势,但使用普通构造函数也可以实现。
我还想说明我在 Reader 中不喜欢的地方。
- 营销。有时我得到的印象是,Reader 是针对所有类型的依赖项进行营销的,如果那是
会话 cookie 或数据库。对我来说,将 Reader 用于几乎不变的对象(例如电子邮件)几乎没有意义
此示例中的服务器或存储库。对于这样的依赖,我发现普通的构造函数和/或部分应用的函数
好多了。本质上,Reader 为您提供了灵活性,因此您可以在每次调用时指定依赖项,但如果您
真的不需要那个,你只需支付它的税。
- 隐式繁重 - 使用不带隐式的 Reader 会使示例难以阅读。另一方面,当你隐藏
使用隐式的嘈杂部分并产生一些错误,编译器有时会让您难以破译消息。
- 与
pure、local 的仪式并为此创建自己的配置类/使用元组。阅读器强迫你添加一些代码
这与问题域无关,因此在代码中引入了一些噪音。另一方面,一个应用程序
使用构造函数的通常使用工厂模式,这也来自问题域之外,所以这个弱点不是
严重。
如果我不想将我的类转换为具有函数的对象怎么办?
你想要的。从技术上讲,您可以避免这种情况,但看看如果我不将 FindUsers 类转换为对象会发生什么。相应的理解行如下所示:
getAddresses <- ((ds: Datastore) => new FindUsers(ds).inactive _).local[Config](_.dataStore)
这不是那么可读,是吗?关键是 Reader 对函数进行操作,所以如果你还没有它们,你需要内联构造它们,这通常不是那么漂亮。