【问题标题】:transformations of data structures with structural sharing具有结构共享的数据结构转换
【发布时间】:2021-09-25 17:24:55
【问题描述】:

在函数式编程中,可以通过使用结构共享来节省大量内存。例如,这两个列表是相同的,但第二个在内存中以更有效的方式表示:

val n = 4
// n: Int = 4
val l1 = List.tabulate(n)(x => (n-x until n).toList)
// l1: List[List[Int]] = List(List(), List(3), List(2, 3), List(1, 2, 3))
val l2 = List.unfold((n, List.empty[Int])) { case (i, l) =>
  if (i > 0) Some((l, ((i - 1), i - 1 :: l)))
  else None
}
// l2: List[List[Int]] = List(List(), List(3), List(2, 3), List(1, 2, 3))

但是当你真正用l2做某事的那一刻,这个优势很快就消失了。 l2.map(_.map(_ + 1)) 的结果不仅需要与 l1 一样多的内存,而且效率也很低,因为它会执行 n*(n-1)/2 加法,即使此数据结构中只有 n-1 不同的数字。

使用可变数据结构,这很容易:您可以更新适当的值,以及告诉您操作是否已在该节点上执行的标记。这样,您只需遍历一次数据结构,保留结构共享,您只需执行n-1 加法。

但是有没有一些优雅的、实用的方法可以在不使用可变数据结构的情况下实现这一点?

【问题讨论】:

  • 为什么说tabulate()的结果和unfold()的结果不一样呢? ScalaDocs 页面似乎没有表明这一点。
  • 只有在您确定没有其他人也引用 l2 或其任何内部列表时,您的可变方法才有效。如果您完全确定这一点,那么您可以使用某种可变单元类,例如final class MutableBox[A](var a: A),然后使用List[Cell[Int]],您还需要保留对最长列表的引用,因为这是需要被更新。 - 然而,你实际上可能对数据使用某种惰性视图会更好,比如为什么甚至计算中间步骤,只使用最大的列表并在需要时派生其他步骤
  • 改写你的问题:“有没有一些优雅的方法可以在不可变的情况下改变事物”?或许,这样一来,答案就更明显了?
  • Dima,这不是我的问题的改写,而是你刚刚编造的。我真正的问题是:是否有可能在不使用可变性的情况下实现可变解决方案的性能(即线性性能而不是二次性能)?
  • @jwvh 我相信重点是在tabulate 版本中,有四个单独的内部列表由对toList 的四次调用创建,而在unfold 版本中,有一个内部@987654337外部列表中的@ 和更早的元素只是后面元素的tail

标签: scala functional-programming structural-sharing


【解决方案1】:

据我所知,标准库中的可变集合没有利用结构共享,因此您需要自己实现一个。

l2 的结构共享有点巧合:它是如何构建的副作用。它没有对底层结构进行编码(在这种情况下,nth 元素是 n+1th 元素的尾部,最后一个元素是所有底层元素)。但是您可以相当容易地对该结构进行编码(我怀疑它可能比可变版本更容易)。

使用 2.13 API

case class TailsFirst[A](val elements: List[A]) extends Seq[List[A]] {
  def length: Int = 1 + elements.length

  def apply(idx: Int): List[A] = {
    require(idx < length)
    underlying.drop(elements.size - idx)
  }

  // useful for iteration, which will happen a lot (e.g. in toString)
  lazy val reverseElements: List[A] = elements.reverse

  def iterator(): Iterator[List[A]] =
    new Iterator[List[A]] {
      var state: Option[List[A]] = Some(reverseElements)
      var toEmit: List[A] = Nil

      def hasNext: Boolean = state.nonEmpty
      def next(): List[A] = {
        if (hasNext) {
          val ret = toEmit
          state.flatMap(_.headOption) match {
            case None =>
              assume(state.contains(Nil))  // hint for a mythical static analyzer
              state = None
              ret

            case Some(toPrepend) =>
              state = state.map(_.tail)
              toEmit = toPrepend :: toEmit
              ret 
          }
        } else throw new NoSuchElementException("exhausted iterator")

  def flatMapElements[B](f: A => List[B]): TailsFirst[B] =
    TailsFirst(elements.flatMap(f))

  def mapElements[B](f: A => B): TailsFirst[B] =
    TailsFirst(elements.map(f))
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-10-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多