无标记最终模式让我们编写纯函数程序,明确它们所需的效果。
但是,扩展此模式可能会变得具有挑战性我将尝试用一个例子来证明这一点。想象一个简单的程序,它从数据库中读取记录并将它们打印到控制台。我们将需要一些自定义类型类Database
和Console
,以及来自cats / scalaz的Monad
以组成它们:
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))
当我想为内层中的函数添加新效果时,问题就开始了。例如,如果没有找到记录,我希望我的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
的结果。此外,在实际应用中,您会在顶层看到很长的效果链。这感觉就像一个代码味道,但我无法指出我能采取的其他方法。很想得到你对此的见解。
谢谢。
我们也可以说这泄漏了实现细节 - 主要不关心日志记录,它只需要读取的结果。此外,在实际应用中,您会在顶层看到很长的效果链。这感觉就像一个代码味道,但我无法指出我能采取的其他方法。
我实际上相信相反的是真的。纯FP的一个关键承诺是等式推理,作为从它的签名中导出方法实现的一种手段。如果read
需要记录效果才能完成它的业务,那么无论如何都应该在签名中以声明方式表达。明确你的影响的另一个好处是,当它们开始积累时,或许我们需要重新考虑这个特定方法正在做什么并将其拆分成更小的组件?或者这种效果真的应该在这里使用吗?
确实,效果会叠加,但正如评论中提到的@TravisBrown一样,它通常是调用堆栈中最高的位置,必须“承受”实际上为整个调用树提供所有隐式证据的后果。