Scala 使用foldRight反转列表的优雅方式?

Scala 使用foldRight反转列表的优雅方式?,scala,Scala,我在《Scala中的编程》(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]()

我在《Scala中的编程》(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经验。因此,我的问题是:

  • 在问题解决方法中,您如何识别/区分foldLeft/foldRight
  • 有没有更好的方法不用

列表上的操作有意不对称。列表数据结构是一个单链表,其中每个节点(数据和指针)都是不可变的。这种数据结构背后的思想是,通过引用内部节点并添加指向它们的新节点,在列表的前面执行修改——列表的不同版本将共享列表末尾的相同节点

将新元素附加到列表末尾的
操作符必须创建整个列表的新副本,否则它将修改与所附加列表共享节点的其他列表。这就是为什么
需要线性时间的原因。 Scala有一个名为
ListBuffer
的数据结构,您可以使用它来代替
操作符,以加快列表末尾的追加速度。基本上,创建一个新的
ListBuffer
,它以一个空列表开始。
ListBuffer
维护一个与程序所知道的任何其他列表完全分离的列表,因此可以通过在末尾添加内容来安全地修改它。在结束添加后,调用
ListBuffer.toList
,将列表释放到世界中,此时如果不复制列表,就不能再添加到结尾

foldLeft
foldRight
也有相似的不对称性
foldRight
要求您浏览整个列表,直到列表的末尾,并在访问过程中跟踪您访问过的任何地方,这样您就可以按相反的顺序访问它们。这通常是递归完成的,它可能导致
foldRight
在大型列表上导致堆栈溢出<另一方面,code>foldLeft按照节点在列表中出现的顺序处理节点,因此它可以忘记已经访问过的节点,一次只需要知道一个节点。虽然
foldLeft
通常也是递归实现的,但它可以利用称为尾部递归消除的优化,在这种优化中,编译器将递归调用转换为循环,因为函数在从递归调用返回后不做任何事情。因此,
foldLeft
即使在很长的列表上也不会溢出堆栈编辑
foldRight
在Scala 2.8中实际上是通过反转列表并在反转的列表上运行
foldLeft
来实现的——因此尾部递归问题不是问题——两种数据结构都正确地优化了尾部递归,您可以选择其中一种(现在,您正在定义
reverse
中的
reverse
——如果您是为了好玩而定义自己的reverse方法,您不必担心,但是如果您是定义Scala的reverse方法,您根本就没有foldRight选项。)

因此,您应该更喜欢
foldLeft
而不是
foldRight


(在将
foldLeft
:::
foldRight
相结合的算法中,您需要自己决定哪一个更重要:堆栈空间还是运行时间。或者您应该将
foldLeft
列表缓冲区一起使用。)由于标准库中
列表
上的
foldRight
是严格的,并且使用线性递归实现,因此通常应避免使用它。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))
所以你看,如果两者都是严格的,那么foldRight和foldLeft中的一个或另一个将被实现(不管从概念上讲)使用
reverse
。由于列表的构造方式与右侧的
关联,直接的迭代折叠将是
foldLeft
,而
foldRight
只是“反向然后foldLeft”

直觉上,您可能会认为这是foldRight的一个缓慢实现,因为它会将列表折叠两次。但是:

  • “tweep”是一个常数因子,所以它渐近等价于折叠一次
  • 无论如何,你必须检查列表两次。一次是将计算推到堆栈上,另一次是将计算从堆栈中弹出
  • 上述
    foldRight
    的实现速度比标准库中的快

  • 在foldRight的严格实现中,这是一种完全错误的二分法。使用线性递归折叠关联到右侧的列表实际上比反转列表并向左折叠要慢。在标准库中实现的情况下,没有理由在列表上使用foldRight。这些问题在几个月前,e Scala辩论邮件列表:我刚刚检查了源代码:
    foldRight=reversed.foldLeft
    ,所以我会更新我的答案。@Ken,是的,我知道可变
    ListBuffer
    类。我实际上在寻找一种比我使用的更好的FP方法。谢谢。@Randall,谢谢你的链接。现在我知道我不会使用foldRig了当我有foldLeft.:)实现
    foldRight