【问题标题】:How to get distinct items from a Scala Iterable, maintaining laziness如何从 Scala Iterable 中获取不同的项目,保持惰性
【发布时间】:2025-12-05 17:00:02
【问题描述】:

我有一个 java.lang.Iterable 懒惰地计算它的值。我正在从 Scala 访问它。是否有只返回不同值的核心 API 方式?例如,成像有一个过滤器方法,它也提供了迄今为止返回的所有结果:

val myLazyDistinctIterable = iterable.filter((previousReturnedItems, newItem) => previousReturnedItems.contains(newItem))

我想这不是一个非常普遍的情况,因为它涉及存储以前返回的项目,这可能是它不在核心 API 中的原因。

我知道List.distinctSets,但我想要一些在被问到之前不会计算其元素的东西。

【问题讨论】:

  • 我认为最简单的方法是在其闭包中返回一个带有可变 Set 的迭代器,该迭代器跟踪它所看到的内容,然后由 filter 生成应用于其输入,更新所见集并酌情返回 false。

标签: scala collections lazy-evaluation


【解决方案1】:

更新仔细阅读问题是好的。这个解决方案没有懒惰。对不起。

toSet 会做你想做的事:

  1. 将迭代的元素存储在集合中(不是您想要的,而是必需的)
  2. 删除/替换重复项

例子

val it = Seq(1,2,3,4,2,4): Iterable[Int]
it.toSet
// Set(1,2,3,4)

如果你觉得花哨,可以将其转换回可迭代对象:

it.toSet.toIterable

或者,拉皮条Iterable

implicit class UniquableIterable[T](t: Iterable[T]) {
  def unique = t.toSet.toIterable
}

然后调用

it.unique

【讨论】:

  • 别担心,猜你不是一个懒惰的人;)
  • @DanGravell 是的...我不认为你会写一个内部状态非常丑陋的UniqueIterable
【解决方案2】:

您可以在Stream 上使用distinct 方法。例如,如果你有这个Iterable

val it = new java.lang.Iterable[Int] {
  def iterator = new java.util.Iterator[Int] {
    var i = 0
    var first = true

    def hasNext = true
    def next =
      if (first) { first = false; i } else { first = true; i += 1; i - 1 }
    def remove() { throw new UnsupportedOperationException("Can't remove.") }
  }
}

你可以写:

scala> import scala.collection.JavaConverters._
import scala.collection.JavaConverters._

scala> val s = it.asScala.toStream
s: scala.collection.immutable.Stream[Int] = Stream(0, ?)

scala> s.take(10).toList
res0: List[Int] = List(0, 0, 1, 1, 2, 2, 3, 3, 4, 4)

scala> val s = it.asScala.toStream.distinct
s: scala.collection.immutable.Stream[Int] = Stream(0, ?)

scala> s.take(10).toList
res1: List[Int] = List(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)

我们可以看出一切都是适当的惰性,因为流是无限的。

【讨论】:

  • D'oh,应该检查 Stream。谢谢。我现在记得 Stream 存储它以前返回的项目。我最初认为这是一个实现细节,但我想这也是其语义的一部分。
  • 你有没有尝试过使用 Scalas 自己的 distinct? x.toList.distinct
【解决方案3】:

这应该可以完成工作(但我讨厌):

class UniqueIterable[T](i: Iterable[T]) extends Iterable[T] {
  import scala.collection.mutable.Set
  def iterator = new Iterator[T] {
    val it = i.iterator
    var nextE: Option[T] = None
    val seen: Set[T] = Set.empty
    def hasNext = {
      popNext()
      nextE.isDefined
    }
    def next = {
      popNext()
      val res = nextE.get
      nextE = None
      res
    }

    @tailrec
    private def popNext() {
      if (nextE.isEmpty && it.hasNext) {
        val n = it.next
        if (seen contains n) popNext()
        else {
          seen += n
          nextE = Some(n)
        }
      }
    }
  }
}

【讨论】:

    【解决方案4】:

    扩展我上面的评论,但我现在无法测试:

    def unique[A](it: Iterator[A]): Iterator[A] = {
      val seen = mutable.Set[A]()
      it.filter { a =>
        if (seen(a))
          false
        else {
          seen += a
          true
        }
      }
    }
    

    至少你明白了。然后,您将其应用于从可迭代对象中获得的迭代器,而不是获得 Stream 的不必要的存储行为。

    【讨论】:

      【解决方案5】:

      这是将.disctinct 方法添加到Iterator 的代码。

      implicit class IteratorWrapper[T](it: Iterator[T]) {
          def distinct = new Iterator[T] {
              var seen = Set.empty[T]
              var ahead = Option.empty[T]
      
              def searchAhead {
                  while (ahead.isEmpty && it.hasNext) {
                      val v = it.next
                      if (!seen(v)) {
                          seen += v
                          ahead = Some(v)
                      }
                  }
              }
      
              def hasNext = {
                  searchAhead
                  ahead.nonEmpty
              }
      
              def next = {
                  searchAhead
                  val result = ahead.get
                  ahead = None
                  result
              }
          }
      }
      

      请注意,与迭代器通常一样,原始迭代器在调用 .distinct 后无效。

      【讨论】:

      • 这是错误的!考虑:Iterator(1, 1)。在第一次调用next 之后,hasNext 仍然是 true,但调用 next 实际上会抛出。
      • 我确实做到了,一切都很好。 pastebin.com/NPAjq2aS
      • 嗯,是的...因为您在原始迭代器上调用 next,而不是通过调用 distinct 创建的迭代器。
      • 我已经解决了这个问题。结果是即使.hasNext 也可以改变迭代器,因为无法判断不同的迭代器是否还有任何元素,你必须偷看。