Scala 2.13 views vs LazyList



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

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

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

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流。当然,有些操作像distinct一样使用O(n)内存,但至少之后的所有操作都可以进行流式传输,而无需重建数据结构。

有了这一点,我开始思考为什么我们需要一个观点,因为它们的力量比以前小得多(即使我可以相信2.13已经解决了这个力量引入的其他一些问题(。我寻找答案,找到了一些暗示,但没有发现足够全面的东西。以下是我的研究:

  • 2.13中的视图描述
  • StackOverflow:List.view和LazyList之间有什么区别
  • 外部网站上的另一个比较

可能是我,但即使在阅读了这些参考文献后,我也没有发现使用视图对大多数用例(如果不是全部的话(有明显的好处。有谁比我更开明?

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

View是最简单的延迟序列,几乎没有额外成本。在一般情况下,最好在默认情况下使用,以避免在处理大序列时分配中间结果。

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

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

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

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

Scala2.12中的Stream比Views或Iterator慢很多。我不确定这是否适用于Scala2.13中的LazyList。


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

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

在您的用例中,我相信Iterator是最合适的:

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

最新更新