【发布时间】: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