无标记的最终模式使我们能够编写有关其所需效果的纯粹功能程序。
但是,扩展这种模式可能会变得具有挑战性。我将尝试以示例为例。想象一个简单的程序,该程序读取数据库的记录并将其打印到控制台。我们还需要一些自定义的类型Database
和Console
,除了CAT/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))
当我想为内层的函数添加新的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
的结果。同样,在实际应用中,您会看到顶层的效果非常长的链条。感觉就像是一个密码,但我不能用手指戴上我可以采用的其他方法。
很想了解您的见解。
谢谢。
我们还可以说这泄漏了实施细节 - 主要不 关心记录,只需要阅读的结果即可。另外,真实 您看到的应用程序在顶层中确实很长的效果链。 感觉就像是代码杂志,但我不能把我的手指放在其他地方 我可以采取的方法。
我实际上认为相反。纯FP的关键承诺之一是方程推理,作为从其签名中得出实现方法的一种手段。如果read
需要采用记录效果才能进行业务,则应在签名中声明地表达它。明确您效果的另一个优点是,当它们开始积累时,也许我们需要重新考虑这种特定方法正在做的事情并将其分为较小的组件?还是应该在这里真正使用此效果?
的确,效果堆积了,但是正如评论中提到的 @travisbrown,通常是呼叫堆栈中最高的位置,必须"遭受后果",即实际为整个呼叫树提供所有隐含的证据。