如何为这个例子建模
如何用Reader monad建模?
我不确定是否应该使用Reader进行建模,但是可以通过以下方式实现:
- 将类编码为函数,从而使代码与Reader更好地配合
- 用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 forrehension看起来更好。注意,结果函数是咖喱的。它还将以前的构造函数参数作为第一个参数(参数列表)。那样
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()
}
}
这里要注意的一件事是,特定功能不依赖于整个对象,而仅依赖于直接使用的部分。在OOP版本中,UserReminder.emailInactive()
实例将userFinder.inactive()
在此处调用的地方只是调用inactive()
-在第一个参数中传递给它的函数。
请注意,该代码展示了问题中的三个理想属性:
- 显然每个功能需要什么样的依赖关系
- 隐藏一种功能对另一种功能的依赖性
retainUsers
方法不需要了解数据存储依赖项
建模步骤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,因此可能不会完全欣赏它们。
- 统一性-理解的长度有多短,它都不是什么大问题,它只是一个Reader,您可以轻松地将其与另一个实例进行组合,也许只引入了一个Config类型,并
local
在其上进行了一些调用。这一点是IMO的问题,因为您使用构造函数时,没有人会阻止您编写自己喜欢的东西,除非有人做一些愚蠢的事情,例如在构造函数中进行工作,这在OOP中被认为是不好的做法。
- Reader是一种monad,因此可以获得与该方法有关的所有好处-
sequence
,这些traverse
方法是免费实施的。
- 在某些情况下,您可能会发现最好只构建一次Reader并将其用于各种Config。使用构造函数时,没有人会阻止您这样做,您只需要为每个传入的Config重新构建整个对象图即可。尽管我对此没有任何问题(我甚至更喜欢在对应用程序的每个请求中都这样做),但是对于我来说,由于我可能只是出于推测的原因,这对许多人来说并不是一个显而易见的主意。
- Reader会促使您更多地使用函数,这些函数将以FP风格编写的应用程序更好地发挥作用。
- 读者将关注点分开;您可以创建,与所有事物进行交互,定义逻辑而无需提供依赖关系。实际稍后再提供。(感谢Ken Scrambler)。这通常是Reader的优势,但普通构造函数也可以实现。
我还想告诉我我在Reader中不喜欢的内容。
- 行销 有时我会觉得,Reader是针对各种依赖而销售的,如果是会话cookie或数据库,则没有区别。对我而言,将Reader用于几乎恒定的对象(如本例中的电子邮件服务器或存储库)几乎没有意义。对于这种依赖性,我发现普通的构造函数和/或部分应用的函数会更好。从本质上讲,Reader为您提供了灵活性,因此您可以在每次调用时指定依赖项,但是如果您确实不需要,则只需支付其税款。
- 隐式繁重-使用没有隐式含义的Reader会使示例难以阅读。另一方面,当您使用隐式方法隐藏嘈杂的部分并产生一些错误时,编译器有时会使您难以理解消息。
- 使用进行仪式
pure
,local
并创建自己的Config类/为此使用元组。读者会强迫您添加一些与问题域无关的代码,从而在代码中引入一些干扰。另一方面,使用构造函数的应用程序通常使用工厂模式,该模式也来自问题域之外,因此这种弱点并不严重。
如果我不想将类转换为带有函数的对象怎么办?
你要。从技术上讲,您可以避免这种情况,但是只要看看我不将FindUsers
类转换为对象会发生什么。用于理解的相应行如下所示:
getAddresses <- ((ds: Datastore) => new FindUsers(ds).inactive _).local[Config](_.dataStore)
这不是那么可读吗?关键是Reader可以对函数进行操作,因此,如果您还不具备这些功能,则需要内联构造它们,这通常不是那么漂亮。