我正在阅读Programming in Scala
书中关于折叠技巧的文章,并且遇到了这个片段:
def reverseLeft[T](xs:List[T]) = (List[T]() /: xs) {
(y,ys) => ys :: y
}
如您所见,它是使用foldLeft
或/:
运算符完成的。好奇如果我使用:\
做到这一点,我想出了这个:
def reverseRight[T](xs:List[T]) = (xs :\ List[T]()) {
(y,ys) => ys ::: List(y)
}
据我了解,:::
似乎没有::
那么快,并且具有线性成本,具体取决于操作数列表的大小。不可否认,我没有CS的背景,也没有先前的FP经验。所以我的问题是:
:::
的情况下执行此操作? 答案 0 :(得分:12)
由于标准库中foldRight
上的List
是严格的并且使用线性递归实现,因此您应该避免使用它作为规则。 foldRight的迭代实现如下:
def foldRight[A,B](f: (A, B) => B, z: B, xs: List[A]) =
xs.reverse.foldLeft(z)((x, y) => f(y, x))
foldLeft的递归实现可能是这样的:
def foldLeft[A,B](f: (B, A) => B, z: B, xs: List[A]) =
xs.reverse.foldRight(z)((x, y) => f(y, x))
所以你看,如果两者都是 strict ,那么foldRight和foldLeft中的一个或另一个将用reverse
实现(概念上无论如何)。由于列表的构建方式是::
与右侧相关联,因此直接的迭代折叠将为foldLeft
,而foldRight
只是“反向然后折叠左侧”。
直观地说,您可能认为这将是foldRight的缓慢实现,因为它会将列表折叠两次。但是:
foldRight
的实施速度比标准库中的实施速度快。答案 1 :(得分:1)
列表上的操作有意不对称。 List数据结构是单链表,其中每个节点(数据和指针)都是不可变的。这种数据结构背后的想法是,您通过引用内部节点并添加指向它们的新节点来对列表的前面执行修改 - 列表的不同版本将共享列表末尾的相同节点。
将新元素附加到列表末尾的:::
运算符必须创建整个列表的新副本,否则它将修改与您要附加的列表共享节点的其他列表至。这就是:::
占用线性时间的原因。
Scala有一个名为ListBuffer
的数据结构,您可以使用它来代替:::
运算符,以便更快地追加到列表的末尾。基本上,您创建一个新的ListBuffer
,它以一个空列表开头。 ListBuffer
维护一个完全独立于程序知道的任何其他列表的列表,因此可以通过添加到最后来修改它。当您完成添加到最后时,您调用ListBuffer.toList
,将列表发布到世界中,此时您无法再将其添加到最终而不复制它。
foldLeft
和foldRight
也有类似的不对称性。 foldRight
要求您遍历整个列表以到达列表的末尾,并跟踪您在途中访问过的所有位置,以便您以相反的顺序访问它们。这通常是递归完成的,它可能导致foldRight
导致大型列表上的堆栈溢出。另一方面,foldLeft
按照它们在列表中出现的顺序处理节点,因此它可以忘记已经访问过的节点,并且一次只需要知道一个节点。虽然foldLeft
通常也是递归实现的,但它可以利用名为尾递归消除的优化,其中编译器将递归调用转换为循环,因为函数不执行任何操作从递归调用返回后。因此,即使在很长的列表中,foldLeft
也不会溢出堆栈。 编辑:Scala 2.8中的foldRight
实际上是通过反转列表并在反向列表上运行foldLeft
来实现的 - 因此尾递归问题不是问题 - 两个数据结构正确地优化尾递归,你可以选择其中任何一个(你现在就reverse
定义reverse
时遇到了问题 - 你不需要担心,如果你'为了它的乐趣,重新定义你的自己的反向方法,但如果你定义了Scala的反向方法,根本就没有foldRight选项。)
因此,您应该更喜欢foldLeft
和::
而不是foldRight
和:::
。
(在将foldLeft
与:::
或foldRight
与::
合并的算法中,您需要自己决定哪个更重要:堆栈空间或运行时间。或者您应该foldLeft
使用ListBuffer
。)