为什么foldRight和reduceRight不是尾递归?

12

为什么编译器不翻译Scala

(1,2,3,4,5,6).foldRight(10)(_ * _)

转换为Java等效代码

final int[] intArray = new int[]{1,2,3,4,5,6};
int accumulator = 10;

for(int i = intArray.legth - 1; i >=0; i--) {
  accumulator = intArray[i] * accumulator;
}

问题是:为什么foldLeft和reduceLeft是尾递归,但它们的右手对应物不是?

这里有一些链接表明右折叠不是尾递归。我想知道为什么会这样。

如何确定何时使用fold-left,何时使用fold-right?

foldr与foldl(或foldl)的影响

http://programming-scala.labs.oreilly.com/ch08.html


http://www.scala-lang.org/node/5945 - Vasil Remeniuk
3
对于Array来说,foldRightreduceRight 实际上是尾递归的。它们基本上被转化为一个 foldLeft,只不过索引的变化方向相反。 - huynhjl
有一个显而易见的原因,即(1,2,3,4,5,6)是元组,而不是数组。如果您想要一个数组,请使用Array(1,2,3,4,5,6)来调用它。其他人已经回答了一些有趣的算法问题。 - Rex Kerr
2个回答

34

这取决于折叠的方式。 foldLeft 操作会安排

Seq(1, 2, 3).foldLeft(10)(_ - _)

如何将字符串反向排序?

(((10 - 1) - 2) - 3)

(这个数字是4),而foldRight则安排

Seq(1, 2, 3).foldRight(10)(_ - _)

(1 - (2 - (3 - 10)))

现在,假设你正在从袋子里取出数字1、2和3,并使用铅笔和纸进行计算。

对于foldRight情况,您必须这样做:

  1. 从袋子中拿出一个数字n
  2. 在纸上写下“n - ?”
  3. 如果袋子里还有数字,则再次从袋子中取出n,否则转到步骤6。
  4. 擦去问号,并将其替换为“(n - ?)”
  5. 重复第3步。
  6. 擦去问号,并将其替换为10
  7. 执行计算

对于foldLeft情况,您可以像这样做:

  1. 在纸上写下10
  2. 如果袋子里还有数字,则再次从袋子中取出n,否则转到步骤5。
  3. 在已有的表达式旁边写下“ - n
  4. 重复第2步。
  5. 执行计算

但是您不会这样做,因为您也可以像这样做:

  1. 在纸上写下10
  2. 从袋子中取出一个数字n
  3. 将值减去n,擦除该值并写下新值
  4. 重复第2步。

无论袋子里有多少数字,您只需要在纸上写下一个值即可。尾调用消除(TCE)意味着,您可以随着进行累积值的弹出和替换,而不是在堆栈上构建大型递归调用结构。 (即:递归表达式计算基本上以迭代方式执行。)

正如其他人所指出的那样,诸如ArrayLike的随机访问结构使得foldRight操作可以重新排列为foldLeft操作,因此可以符合TCE的条件。 以上描述适用于不可能实现这一点的情况。


该死..铅笔在纸上的比喻非常完美地解释了尾递归问题。干得好! - Freezystem

10

(1, 2, 3, 4, 5, 6)是一个6个值的元组,它没有foldRight方法,但Array(1, 2, 3, 4, 5, 6)有这个方法。

ArrayLike是一个子类化索引序列并具有高效元素访问的特质,这意味着它有某些被优化的方法,包括例如foldRight。每个数组都会隐式地转换为ArrayLike特质的子类。来自Scala trunk:

  @tailrec
  private def foldr[B](start: Int, end: Int, z: B, op: (A, B) => B): B =
    if (start == end) z
    else foldr(start, end - 1, op(this(end - 1), z), op)

字节码:

private static java.lang.Object foldr(scala.collection.IndexedSeqOptimized, int, int, java.lang.Object, scala.Function2);

...

  Code:
   Stack=6, Locals=6, Args_size=5
   0:   iload_1
   1:   iload_2
   2:   if_icmpne   7
   5:   aload_3
   6:   areturn
   7:   aload_0
   8:   iload_2
   9:   iconst_1
   10:  isub
   11:  aload   4
   13:  aload_0
   14:  iload_2
   15:  iconst_1
   16:  isub
   17:  invokeinterface #21,  2; //InterfaceMethod scala/collection/SeqLike.apply:(I)Ljava/lang/Object;
   22:  aload_3
   23:  invokeinterface #72,  3; //InterfaceMethod scala/Function2.apply:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
   28:  astore_3
   29:  istore_2
   30:  astore_0
   31:  goto    0
  LineNumberTable: 
   line 68: 0
   line 67: 6
   line 69: 7

编辑:字节码中的方法是迭代的,这意味着编译器必须应用尾调用优化。

若没有有效的元素访问方式(如有效的apply方法),通常情况下最好的做法是使用迭代器和非尾递归函数实现foldRight,或通过构建一个新集合来反转集合并在其上执行foldLeft(目前采用后一种方式)。对于所有具有有效随机访问的序列,将覆盖并进行了优化。


1
实际上,这是迭代的,而不是递归的。 - Peter Lewerin
4
将代码改为尾递归的目的在于编译器可以将其优化为迭代。因此,如果递归代码能够编译成迭代代码,则在Scala中它就是尾递归的。很容易观察到代码是递归的。 - Daniel C. Sobral
我的观点是,对于一个初学者,指着迭代代码并说“看起来像尾递归”似乎没有什么帮助。@Daniel - Peter Lewerin
1
不是故意无礼 - 所以我已经删除了那条评论 :) - axel22
@Hoodiecrow - 最后一次编辑的答案应该让它更清楚一点 :) - axel22
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接