【发布时间】:2019-02-20 07:32:31
【问题描述】:
tagless-final 模式让我们可以编写纯函数式程序,这些程序会明确说明它们需要的效果。
但是,扩展此模式可能会变得具有挑战性。我将尝试用一个例子来证明这一点。想象一个简单的程序,它从数据库中读取记录并将它们打印到控制台。除了来自cats/scalaz 的Monad 之外,我们还需要一些自定义类型类Database 和Console 来组合它们:
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的结果。此外,在实际应用程序中,您会在顶层看到非常长的效果链。感觉就像是代码味道,但我无法确定我还能采取什么其他方法。
很想听听您对此的见解。
谢谢。
【问题讨论】:
-
日志记录是一种“能力”,而不是实现细节。所以“我们也可以说这泄露了实现细节——main 不关心日志”这句话是不正确的。
-
关于“
read链上的所有调用者”:您在这里调用的层形成一棵树,虽然确实靠近树的底部(您的main) ,你被困在枚举所有依赖项所需的所有效果,这些树a)在实践中往往不会非常深,b)基础附近的实现应该是相当少的,只是组成具有更少要求和更多逻辑的部分.
标签: scala functional-programming software-design tagless-final