如何对这个例子建模
如何使用Reader Monad对此进行建模?
我不确定是否应该使用Reader,但可以通过以下方式实现:
- 将类编码为函数,使得代码更适合与Reader一起使用
- 在for循环中将函数与Reader组合,并使用它
就在开始之前,我需要告诉你关于这个答案的一些小样本代码调整。第一个更改是关于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 = ???
}
变成
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()
函数。
请注意,代码展示了问题中的三个理想属性:
1.清楚地知道每个功能需要哪些依赖项
2.隐藏一个功能对另一个功能的依赖关系
3.
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。
- 统一性 - 无论for comprehension多长,它只是一个Reader,您可以轻松地将其与另一个实例组合起来,也许只需引入一个Config类型并在其上添加一些“local”调用即可。这一点在我看来更多是口味问题,因为当您使用构造函数时,除非有人做了一些愚蠢的事情(例如在构造函数中执行工作,这在OOP中被认为是一种不良实践),否则您可以组合任何您喜欢的东西。
- Reader是一个monad,因此它获得了所有相关的好处 - “sequence”,“traverse”方法都是免费实现的。
- 在某些情况下,您可能会发现仅构建一次Reader并将其用于广泛的Configs更可取。使用构造函数时,没有人会阻止您这样做,只需为每个传入的Config重新构建整个对象图即可。虽然我对此没有问题(我甚至更喜欢在每个请求到达应用程序时执行此操作),但出于我只能猜测的原因,这不是许多人的明显想法。
- Reader推动您更多地使用函数,这将与主要以FP风格编写的应用程序更好地配合。
- Reader分离关注点;您可以创建、交互和定义逻辑而不提供依赖项。实际上稍后单独提供。 (感谢Ken Scrambler提供此观点)。这通常是Reader的优点,但是使用普通构造函数也可以实现。
我也想说一下我不喜欢Reader的地方。
- 市场营销。有时我会有这样的印象,即Reader被用于各种依赖项的营销,而不区分是会话cookie还是数据库。对我来说,对于诸如电子邮件服务器或存储库之类的几乎恒定的对象,使用Reader没有什么意义。对于这样的依赖项,我发现普通的构造函数和/或部分应用的函数更好。基本上,Reader为您提供了灵活性,以便您可以在每次调用时指定依赖项,但如果您真的不需要它,那么您只需支付其税费。
- 隐式繁重 - 如果没有隐式地使用Reader将使示例难以阅读。另一方面,当您使用隐式隐藏嘈杂的部分并出现错误时,编译器有时会给您难以解释的消息。
- 使用“pure”,“local”和创建自己的Config类/使用元组的仪式。Reader强制您添加一些与问题域无关的代码,从而在代码中引入了一些噪音。另一方面,使用构造函数的应用程序通常使用工厂模式,这也是来自问题域之外的,因此这种弱点并
如果我不想用函数将我的类转换为对象怎么办?
你可以,从技术上讲,避免这种做法,但是看看如果我不将 FindUsers
类转换为对象会发生什么。相应的 for 推导式行将如下:
getAddresses <- ((ds: Datastore) => new FindUsers(ds).inactive _).local[Config](_.dataStore)
读起来不是很易懂,对吧?重点是Reader操作函数,因此如果您没有它们,您需要在内联中构建它们,这通常不是很美观。