【问题标题】:scala view filter not lazy?scala视图过滤器不懒惰?
【发布时间】:2023-03-18 06:20:02
【问题描述】:

在尝试了解流、迭代器和集合视图之间的区别时,我偶然发现了以下奇怪的行为。

这里是代码(map 和 filter 只是打印它们的输入并原封不动地转发):

object ArrayViewTest {
  def main(args: Array[String]) {
    val array = Array.range(1,10)

    print("stream-map-head: ")
    array.toStream.map(x => {print(x); x}).head

    print("\nstream-filter-head: ")
    array.toStream.filter(x => {print(x); true}).head

    print("\niterator-map-head: ")
    array.iterator.map(x => {print(x); x}).take(1).toArray

    print("\niterator-filter-head: ")
    array.iterator.filter(x => {print(x); true}).take(1).toArray

    print("\nview-map-head: ")
    array.view.map(x => {print(x); x}).head

    print("\nview-filter-head: ")
    array.view.filter(x => {print(x); true}).head
  }
}

及其输出:

stream-map-head: 1
stream-filter-head: 1
iterator-map-head: 1
iterator-filter-head: 1
view-map-head: 1
view-filter-head: 123456789    // <------ WHY ?

为什么在视图上调用过滤器会处理整个数组? 我希望通过调用 head 只驱动一次过滤器的评估,就像在所有其他情况下一样,特别是在使用 map on view 时。

我缺少哪些见解?

(评论的小问题,为什么迭代器上没有头?)

编辑: scala.collection.mutable.ArraySeq.range(1,10)scala.collection.mutable.ArrayBuffer.range(1,10)scala.collection.mutable.StringBuilder.newBuilder.append("123456789") 实现了相同的奇怪行为(如这里的 scala.Array.range(1,10))。 但是,对于所有其他可变集合和所有不可变集合,视图上的过滤器按预期工作并输出1

【问题讨论】:

  • (mini-side-answer) 如果您想获得迭代器的第一个元素,则必须使用 next 推进它并调用它,例如两次会有问题。

标签: scala scala-collections


【解决方案1】:

似乎head 使用isEmpty

trait IndexedSeqOptimized[+A, +Repr] extends Any with IndexedSeqLike[A, Repr] { self =>
...
override /*IterableLike*/
def head: A = if (isEmpty) super.head else this(0)

isEmpty 使用length

trait IndexedSeqOptimized[+A, +Repr] extends Any with IndexedSeqLike[A, Repr] { self =>
  ...
  override /*IterableLike*/
  def isEmpty: Boolean = { length == 0 }

length 的实现是从 Filtered 使用的,它循环整个数组

trait Filtered extends super.Filtered with Transformed[A] {
  protected[this] lazy val index = {
    var len = 0
    val arr = new Array[Int](self.length)
    for (i <- 0 until self.length)
      if (pred(self(i))) {
        arr(len) = i
        len += 1
      }
    arr take len
  }
  def length = index.length
  def apply(idx: Int) = self(index(idx))
}

Filtered trait 仅在调用 filter 时使用

protected override def newFiltered(p: A => Boolean): Transformed[A] =
 new { val pred = p } with AbstractTransformed[A] with Filtered

这就是为什么在使用 filter 而不是在使用 map 时会发生这种情况

【讨论】:

  • 感谢您的跟踪!事实上,这个问题似乎只发生在 IndexedSeq 特征上(参见我编辑的帖子)。您是否同意这种实现方式是对性能的愚蠢浪费,并且可能被称为错误?忽略任何内部必要性:对我来说,通过单独计算数组的元素来计算数组的长度听起来绝对没有必要——实际上 O(1) 访问数组的长度是数组的一个关键属性,事实上,关于数组长度 is 在那里 - 它根本不被过滤器读取。
  • 我同意性能并未针对您的情况进行优化。我不知道这是一种权衡还是可以避免。也许你可以问gitter.im/scala/contributors
  • 澄清一下:Scala 不是通过计算数组的元素来计算数组的长度。它通过计算视图的元素来计算视图的长度,但数组的视图不是数组。
【解决方案2】:

我认为Array 必须是一个可变索引序列。它的视图也是一个可变集合:) 因此,当它创建一个视图时,它会创建一个索引,该索引在原始集合和过滤集合之间进行映射。懒惰地创建这个索引并没有真正的意义,因为当有人请求第 i 个元素时,可能会遍历整个源数组。从某种意义上说,在调用head 之前不会创建此索引,这仍然是一种惰性。 scala 文档中仍然没有明确说明这一点,乍一看它看起来像是一个错误。

对于小问题,我认为迭代器上head 的问题在于人们期望head 是纯函数,即你应该能够调用它n 次并且每次都应该返回相同的结果.迭代器本质上是可变的数据结构,根据合同只能遍历一次。这可以通过缓存迭代器的第一个元素来克服,但我发现这非常令人困惑。

【讨论】:

  • 实际上数组viewmap 函数中是惰性的。为什么
  • 因为您不需要在映射视图和源视图之间重建索引。另外我不确定映射视图是否不是副本......非常混乱
猜你喜欢
  • 2016-07-24
  • 2018-11-19
  • 2012-11-04
  • 1970-01-01
  • 1970-01-01
  • 2022-11-13
  • 2019-06-20
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多