无标记final模式让我们可以编写纯粹的函数式程序,这些程序明确地描述了它们所需要的效果。
然而,扩展这种模式可能会变得很有挑战性。我将试着用一个例子来说明这一点。想象一下,一个简单的程序从数据库中读取记录并将其打印到控制台。我们需要一些自定义类型类 Database
以及 Console
,除了 Monad
从cats/scalaz开始,以构成它们:
def main[F[_]: Monad: Console: Database]: F[Unit] =
read[F].flatMap(Console[F].print)
def read[F[_]: Functor: Database]: F[List[String]] =
Database[F].read.map(_.map(recordToString))
当我想给内层的函数添加一个新的a效果时,问题就开始了。例如,我想要我的 read
函数在未找到记录时记录消息
def read[F[_]: Monad: Database: Logger]: F[List[String]] =
Database[F].read.flatMap {
case Nil => Logger[F].log("no records found") *> Nil.pure
case records => records.map(recordToString).pure
}
但是现在,我必须加上 Logger
对的所有调用方的约束 read
在链条上。在这个人为的例子中 main
,但想象一下,这是一个复杂的现实世界应用程序下面的几层。
我们可以从两个方面来看待这个问题:
我们可以说,这是一件好事,明确我们的影响,我们确切地知道哪些影响是需要的每一层
我们也可以说,这泄露了执行细节—— main
不关心日志记录,只需要 read
. 此外,在实际应用中,您可以在顶层看到非常长的效果链。这感觉像是一种代码的味道,但我不知道我可以采取什么其他方法。
我很想听听你的见解。
谢谢。
1条答案
按热度按时间qzlgjiam1#
我们还可以说,这泄漏了实现细节-main不关心日志记录,它只需要读取的结果。此外,在实际应用中,您可以在顶层看到非常长的效果链。这感觉像是一种代码的味道,但我不知道我可以采取什么其他方法。
事实上我认为恰恰相反。纯fp的一个关键承诺是将等式推理作为从其签名中导出方法实现的一种手段。如果
read
需要一个日志效果才能完成它的业务,那么无论如何,它应该在签名中声明性地表示出来。明确你的效果的另一个好处是,当它们开始累积时,也许我们需要重新思考这个特定的方法在做什么,并把它分成更小的部分?或者这个效果真的应该用在这里?确实,效果会叠加起来,但正如@travisbrown在评论中提到的那样,它通常是调用堆栈中的最高位置,必须“承受实际为整个调用树提供所有隐式证据的后果”。