【问题标题】:Tagless-final effect propagation无标签最终效果传播
【发布时间】:2019-02-20 07:32:31
【问题描述】:

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

但是,扩展此模式可能会变得具有挑战性。我将尝试用一个例子来证明这一点。想象一个简单的程序,它从数据库中读取记录并将它们打印到控制台。除了来自cats/scalaz 的Monad 之外,我们还需要一些自定义类型类DatabaseConsole 来组合它们:

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 的结果。此外,在实际应用程序中,您会在顶层看到非常长的效果链。感觉就像是代码味道,但我无法确定我还能采取什么其他方法。

很想听听您对此的见解。

谢谢。

【问题讨论】:

  • 日志记录是一种“能力”,而不是实现细节。所以“我们也可以说这泄露了实现细节——main 不关心日志”这句话是不正确的。
  • 关于“read 链上的所有调用者”:您在这里调用的层形成一棵树,虽然确实靠近树的底部(您的 main) ,你被困在枚举所有依赖项所需的所有效果,这些树a)在实践中往往不会非常深,b)基础附近的实现应该是相当少的,只是组成具有更少要求和更多逻辑的部分.

标签: scala functional-programming software-design tagless-final


【解决方案1】:

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

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

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

【讨论】:

    猜你喜欢
    • 2020-10-07
    • 1970-01-01
    • 1970-01-01
    • 2019-08-28
    • 1970-01-01
    • 1970-01-01
    • 2019-01-02
    • 1970-01-01
    • 2021-08-22
    相关资源
    最近更新 更多