4

tagless-final 模式让我们可以编写纯函数式程序,这些程序明确说明了它们需要的效果。

但是,扩展这种模式可能会变得具有挑战性。我将尝试用一个例子来证明这一点。想象一个简单的程序,它从数据库中读取记录并将它们打印到控制台。我们将需要一些自定义类型类DatabaseConsole,除了Monad来自猫/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))

当我想为内层的函数添加新的效果时,问题就开始了。例如,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,但想象一下这是一个复杂的现实世界应用程序的几层。

我们可以从两个方面来看这个问题:

  1. 我们可以说明确我们的效果是一件好事,而且我们确切地知道每一层需要哪些效果
  2. 我们也可以说这泄露了实现细节——main不关心日志,它只需要read. 此外,在实际应用程序中,您会在顶层看到非常长的效果链。感觉就像是代码气味,但我无法确定我可以采取什么其他方法。

很想听听您对此的见解。

谢谢。

4

1 回答 1

2

我们也可以说这泄露了实现细节——main 不关心日志,它只需要读取的结果。此外,在实际应用程序中,您会在顶层看到非常长的效果链。感觉就像是代码气味,但我无法确定我可以采取什么其他方法。

我实际上相信相反的事实。纯 FP 的主要承诺之一是等式推理,作为从其签名推导方法实现的一种手段。如果read需要一个日志效果来完成它的业务,那么无论如何它应该在签名中声明性地表达。明确您的效果的另一个优点是,当它们开始累积时,也许我们需要重新考虑这个特定方法在做什么并将其拆分为更小的组件?还是真的应该在这里使用这个效果?

确实,效果会叠加,但正如@TravisBrown 在评论中提到的那样,它通常是调用堆栈中的最高位置,它必须“承受后果”,即实际为整个调用树提供所有隐式证据。

于 2019-02-20T08:19:17.547 回答