【问题标题】:Migrate a Traversable that uses a visitor to an Iterable in Scala 2.13将使用访问者的 Traversable 迁移到 Scala 2.13 中的 Iterable
【发布时间】:2021-04-24 19:22:14
【问题描述】:

migration guide to Scala 2.13 解释说 Traversable 已被删除,应该使用 Iterable 代替。这种变化对于一个项目来说尤其烦人,它使用访问者在树的Node 类中实现foreach 方法:

case class Node(val subnodes: Seq[Node]) extends Traversable[Node] {
  override def foreach[A](f: Node => A) = Visitor.visit(this, f)
}

object Visitor {
  def visit[A](n: Node, f: Node => A): Unit = {
    f(n)
    for (sub <- n.subnodes) {
      visit(sub, f)
    }
  }
}

object Main extends App {
  val a = Node(Seq())
  val b = Node(Seq())
  val c = Node(Seq(a, b))
  for (Node(subnodes) <- c) {
    Console.println("Visiting a node with " + subnodes.length + " subnodes")
  }
}

输出:

Visiting a node with 2 subnodes
Visiting a node with 0 subnodes
Visiting a node with 0 subnodes

迁移到 Scala 2.13 的一个简单解决方法是首先将访问的元素存储在 remaining 缓冲区中,然后用于返回迭代器:

import scala.collection.mutable
import scala.language.reflectiveCalls

case class Node(val subnodes: Seq[Node]) extends Iterable[Node] {
  override def iterator: Iterator[Node] = {
    val remaining = mutable.Queue.empty[Node]
    Visitor.visit(this, item => iterator.remaining.enqueue(item))
    remaining.iterator
  }
}

// Same Visitor object
// Same Main object

这个解决方案的缺点是它引入了新的分配给 GC 带来压力,因为访问的元素的数量通常非常大。

您对如何使用现有访问者但不引入新分配从Traversable 迁移到Iterable 有什么建议?

【问题讨论】:

  • 自从new Foo {} 使用反射?不管怎样,看看 Iterator 上的unfold 方法。
  • 好点,我误解了一条错误消息。
  • 您的访问者与代码中的完全一样吗?因为这可以通过LazyList,.flatMap 和递归来实现。
  • @mateusz-kubuszok 原始访问者非常相似(参见here)。由于它在很多地方都被调用,所以我一直在寻找不修改它的解决方案。我仍然看不到LazyList 会如何阻止分配。您的意思是 LazyList 上的迭代器永远不会分配完整列表?
  • 事实上,Iterator 和 Traversable 只是接口,它不依赖于知道前面的所有值。当您执行 Iterator.from(0).drop(1000000).take(4).toList 时,您不会预先分配 1000000 个值。您只在需要时分配每个值。

标签: scala migration iterable visitor-pattern scala-2.13


【解决方案1】:

如您所见,您需要扩展 Iterable 而不是 Traversable。你可以这样做:

case class Node(name: String, subnodes: Seq[Node]) extends Iterable[Node] {
  override def iterator: Iterator[Node] = Iterator(this) ++ subnodes.flatMap(_.iterator)
}

val a = Node("a", Seq())
val b = Node("b", Seq())
val c = Node("c", Seq(a, b))
val d = Node("d", Seq(c))

for (node@Node(name, _) <- d) {
  Console.println("Visiting node " + name + " with " + node.subnodes.length + " subnodes")
}

输出:

Visiting node d with 1 subnodes
Visiting node c with 2 subnodes
Visiting node a with 0 subnodes
Visiting node b with 0 subnodes

然后你可以做更多的操作如:

d.count(_.subnodes.length > 1)

代码在Scastie 运行。

【讨论】:

  • 对于我发布的最小示例来说,这实际上是一个不错的解决方案。但是,原项目还在Node上调用了withFilter等方法。
  • @Federico,目前您似乎也无法进行过滤:scastie.scala-lang.org/toshetah/qUR9I1HwQQaXRuqj2YSAcg
  • 你是对的,在示例中调用filter 会导致堆栈溢出。我可能从原始项目中删除了太多内容。但是,如果您删除 extends: scastie.scala-lang.org/6CFD3kHnTo2oVgFXnQG4xQ,这会起作用并给出编译错误
  • LazyList 一样,这个解决方案需要重新实现访问者的逻辑,但实际上我看不到避免它的方法。我对内部_.iterators 的评估是否正确?这意味着可能会为树的每个级别分配一系列迭代器。
  • @Federico 基本上是我的看法,你可以放弃Visitor。我的代码没有它就可以到达所有节点。请注意,在您的示例中,iterator 只是发起者,visitor 使所有节点都可以访问。但是,是的,同一级别的所有节点都将在迭代器中。但!从头开始 subnodes 被声明为 Seq 的事实意味着这已经完成了。也许您应该考虑将subnodes 保留为iterator 而不是Seq
【解决方案2】:

这是一个示例,您的代码可以使用LazyList 实现并且不需要访问者:

case class Node(val subnodes: Seq[Node]) {
  
  def recursiveMap[A](f: Node => A): LazyList[A] = {
    def expand(node: Node): LazyList[Node] = node #:: LazyList.from(node.subnodes).flatMap(expand)
    expand(this).map(f)
  }
}

val a = Node(Seq())
val b = Node(Seq())
val c = Node(Seq(a, b))

val lazyList = c.recursiveMap { node =>
  println("computing value")
  "Visiting a node with " + node.subnodes.length + " subnodes"
}

println("started computing values")

lazyList.iterator.foreach(println)

输出

started computing values
computing value
Visiting a node with 2 subnodes
computing value
Visiting a node with 0 subnodes
computing value
Visiting a node with 0 subnodes

如果您自己不存储 lazyList 引用并且只存储迭代器,那么 JVM 将能够随时 GC 值。

【讨论】:

    【解决方案3】:

    我们最终编写了一个最小的Traversable trait,只实现了我们代码库中使用的方法。这样就没有额外的开销,也不需要改变访问者的逻辑。

    import scala.collection.mutable
    
    /** A trait for traversable collections. */
    trait Traversable[+A] {
      self =>
    
      /** Applies a function to all element of the collection. */
      def foreach[B](f: A => B): Unit
    
      /** Creates a filter of this traversable collection. */
      def withFilter(p: A => Boolean): Traversable[A] = new WithFilter(p)
    
      class WithFilter(p: A => Boolean) extends Traversable[A] {
        /** Applies a function to all filtered elements of the outer collection. */
        def foreach[U](f: A => U): Unit = {
          for (x <- self) {
            if (p(x)) f(x)
          }
        }
    
        /** Further refines the filter of this collection. */
        override def withFilter(q: A => Boolean): WithFilter = {
          new WithFilter(x => p(x) && q(x))
        }
      }
    
      /** Finds the first element of this collection for which the given partial
        * function is defined, and applies the partial function to it.
        */
      def collectFirst[B](pf: PartialFunction[A, B]): Option[B] = {
        for (x <- self) {
          if (pf.isDefinedAt(x)) {
            return Some(pf(x))
          }
        }
        None
      }
    
      /** Builds a new collection by applying a partial function to all elements
        * of this collection on which the function is defined.
        */
      def collect[B](pf: PartialFunction[A, B]): Iterable[B] = {
        val elements = mutable.Queue.empty[B]
        for (x <- self) {
          if (pf.isDefinedAt(x)) {
            elements.append(pf(x))
          }
        }
        elements
      }
    }
    

    【讨论】:

      猜你喜欢
      • 2021-09-29
      • 2019-08-08
      • 2012-01-24
      • 2011-01-08
      • 2011-03-27
      • 2019-12-21
      • 1970-01-01
      • 1970-01-01
      • 2022-01-10
      相关资源
      最近更新 更多