无标记最终模式允许我们编写纯函数式程序,明确说明它们所需的效果。
但是,扩展此模式可能会变得充满挑战。我将尝试通过一个例子来证明这一点。想象一下一个简单的程序,该程序从数据库中读取记录并将其打印到控制台。除了cats / scalaz中的Database
外,我们还需要一些自定义类型类Console
和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
的结果。此外,在实际应用中,您会在顶层看到很长的效果链。感觉像是在闻代码,但是我不能指望我可以采用其他方法。希望您对此有所了解。
谢谢。
答案 0 :(得分:2)
我们也可以说这会泄漏实现细节-main不会 关心日志记录,只需要读取结果即可。而且,实际上 在应用程序中,您会在顶层看到很长的效果链。 感觉像是代码的气味,但我不能指责其他 我可以采取的方法。
我实际上相信相反的说法是正确的。纯FP的主要承诺之一就是将方程式推理作为从其签名中派生方法实现的一种手段。如果read
需要记录效果以完成业务,则应在声明中以声明方式表示。明确表明您的效果的另一个优势是,当它们开始积累时,也许我们需要重新考虑此特定方法的作用并将其分解为较小的部分?还是应该在这里真正使用这种效果?
确实可以叠加各种效果,但是正如注释中提到的@TravisBrown一样,通常它是调用堆栈中必须“承受后果”的最高位置,实际上需要为整个调用树提供所有隐式证据