tagless-final 模式让我们可以编写纯函数式程序,这些程序明确说明了它们需要的效果。
但是,扩展这种模式可能会变得具有挑战性。我将尝试用一个例子来证明这一点。想象一个简单的程序,它从数据库中读取记录并将它们打印到控制台。我们将需要一些自定义类型类Database
和Console
,除了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
,但想象一下这是一个复杂的现实世界应用程序的几层。
我们可以从两个方面来看这个问题:
- 我们可以说明确我们的效果是一件好事,而且我们确切地知道每一层需要哪些效果
- 我们也可以说这泄露了实现细节——
main
不关心日志,它只需要read
. 此外,在实际应用程序中,您会在顶层看到非常长的效果链。感觉就像是代码气味,但我无法确定我可以采取什么其他方法。
很想听听您对此的见解。
谢谢。