Scala 2.13 视图与 LazyList

Scala 2.13 views vs LazyList

我正在将一个项目从 Scala 2.12.1 迁移到 2.13.6,发现 SeqView#flatMap 现在 returns 一个 View,它没有 distinct 方法。因此,我有一段代码不再编译:

val nodes = debts.view
      .flatMap { case Debt(from, to, _) => List(from, to) }
      .distinct
      .map(name => (name, new Node(name)))
      .toMap

有一种愚蠢的方法可以解决这个问题,将视图转换为序列,然后再返回视图:

val nodes = debts.view
      .flatMap { case Debt(from, to, _) => List(from, to) }.toSeq.view
      .distinct
      .map(name => (name, new Node(name)))
      .toMap

然而,这显然不是很好,因为它强制收集视图,而且必须在类型之间来回切换是非常不雅的。我找到了另一种修复它的方法,就是使用 LazyList:

val nodes = debts.to(LazyList)
      .flatMap { case Debt(from, to, _) => List(from, to) }
      .distinct
      .map(name => (name, new Node(name)))
      .toMap

这就是我想要的,它基本上表现得像 Java 流。当然,一些操作有 O(n) 内存使用,例如 distinct,但至少所有操作都被流式处理,而无需重建数据结构。

有了这个,它让我思考为什么我们应该需要一个视图,因为它们比以前弱得多(即使我相信 2.13 已经解决了这个功能引入的一些其他问题)。我寻找答案并找到了提示,但没有找到足够全面的提示。以下是我的研究:

可能是我,但即使在阅读了这些参考资料之后,对于大多数(如果不是全部)用例,我也没有发现使用视图有明显的好处。还有比我开明的吗?

实际上在 Scala 2.13 中惰性序列有 3 种基本可能性:View、Iterator 和 LazyList。

View 是最简单的惰性序列,附加成本很少。一般情况下最好使用默认值,以避免在处理大型序列时分配中间结果。

可以多次遍历View(使用foreach、foldLeft、toMap等)。每次遍历都会单独执行转换(map、flatMap、filter 等)。因此必须小心,要么避免耗时的转换,要么只遍历视图一次。

Iterator只能遍历一次。它类似于 Java 流或 Python 生成器。 Iterator 上的大多数转换方法都要求您仅使用返回的 Iterator 并丢弃原始对象。

它也像 View 一样快,并且支持更多操作,包括 distinct。

LazyList 基本上是一个真正的严格结构,可以动态自动扩展。 LazyList 记忆所有生成的元素。如果你有一个带有 LazyList 的 val,内存将被分配给所有生成的元素。但是如果你动态遍历它并且不存储在val中,垃圾收集器可以清理遍历的元素。

Scala 2.12 中的 Stream 比视图或迭代器慢得多。我不确定这是否适用于 Scala 2.13 中的 LazyList。


所以每个惰性序列都有一些警告:

  • 视图可以多次执行转换。
  • 迭代器只能使用一次。
  • LazyList 可以为所有的序列元素分配内存。

我认为在您的用例中,迭代器是最合适的:

val nodes = debts.iterator
      .flatMap { case Debt(from, to, _) => List(from, to) }
      .distinct
      .map(name => (name, new Node(name)))
      .toMap