【问题标题】:Fold and foldLeft method differencefold 和 foldLeft 方法的区别
【发布时间】:2026-01-24 23:15:01
【问题描述】:

我不确定 Scala 中的 foldfoldLeft 有什么区别。

问题Difference between fold and foldLeft or foldRight? 有一个关于订购的答案。这是可以理解的。但我仍然不明白为什么这样做(来自 REPL):

scala> Array("1","2","3").foldLeft(0)(_ + _.toInt)
res6: Int = 6

但事实并非如此:

scala> Array("1","2","3").fold(0)(_ + _.toInt)
<console>:8: error: value toInt is not a member of Any
              Array("1","2","3").fold(0)(_ + _.toInt)
                                               ^

这个错误信息是什么意思?

文档中的这一行也让我感到困惑。

z - 折叠操作的中性元素;可以添加到 结果任意次数,并且不得更改结果 (例如,Nil 表示列表连接,0 表示加法,或 1 表示 乘法。)

为什么要添加任意次数?我认为折叠的工作方式不同。

【问题讨论】:

    标签: scala


    【解决方案1】:

    按照 Scala 的定义,foldLeft 是线性运算,而 fold 可以是树运算。例如:

    List(1,2,3,4,5).foldLeft(0)(_ + _)
    // This is the only valid order of operations
    0+1 = 1
          1+2 = 3
                3+3 = 6
                      6+4 = 10
                            10 + 5 = 15
                                     15  // done
    
    List(1,2,3,4,5).fold(0)(_ + _)
    // This is valid
    0+1 = 1             0+3 = 3           0+5 = 5
          1+2 = 3             3+4 = 7           5
                3         +         7=10        5
                                      10    +   5 = 15
                                                    15  // done
    

    为了允许顺序列表的任意树分解,你必须有一个不做任何事情的零(所以你可以在树中任何你需要的地方添加它)并且你必须创建相同的东西您将其作为二进制参数,因此类型不会因您分解树的方式而改变。

    (能够以树的形式进行评估非常适合并行化。如果您希望能够随时转换输出时间,则需要组合运算符 标准起始值-transforms-sequence-element-to-desired-type function就像foldLeft有。Scala有这个并称之为aggregate,但在某些方面这更像foldLeft而不是fold。)

    【讨论】:

    • 我喜欢这个解释,干得好,但它没有回答问题。问题是“为什么折叠示例不起作用?”。 fold 是并行的,这使得 init 值和结果必须是集合的超类型,这就是这个 fold 示例不起作用的原因。
    • @CarlosVerdes - 我认为您没有仔细阅读我的答案。在示例块之后的段落中,我准确地解释了您所说的重点。除了我不仅解释了问题所在,还解释了原因。
    【解决方案2】:

    我不熟悉 Scala,但 Scala 的集合库与 Haskell 的设计相似。这个答案基于 Haskell,对于 Scala 也可能是准确的。

    因为foldLeft 从左到右处理其输入,所以它可以有不同的输入和输出类型。另一方面,fold 可以按各种顺序处理其输入,因此输入和输出必须具有相同的类型。这通过展开折叠表达式最容易看到。 foldLeft 按特定顺序运行:

    Array("1","2","3").foldLeft(0)(_ + _.toInt)
    = ((0 + "1".toInt) + "2".toInt) + "3".toInt
    

    请注意,数组元素永远不会用作组合函数的第一个参数。它们总是出现在+ 的右侧。

    fold 不保证特定顺序。它可以做各种各样的事情,例如:

    Array("1","2","3").fold(0)(_ + _.toInt)
    =  ((0 + "1".toInt) + "2".toInt) + "3".toInt
    or (0 + "1".toInt) + ("2" + "3".toInt).toInt
    or "1" + ("2" + ("3" + 0.toInt).toInt).toInt
    

    数组元素可以出现在组合函数的任一参数中。但是你的组合函数期望它的第一个参数是一个 int。如果您不遵守该约束,您最终会将字符串添加到整数!此错误被类型系统捕获。

    可以多次引入中性元素,因为通常通过拆分输入并执行多个顺序折叠来实现并行折叠。顺序折叠一次引入了中性元素。想象一下Array(1,2,3,4).fold(0)(_ + _) 的一次特定执行,其中数组被拆分为两个单独的数组,并且这些数组在两个线程中按顺序折叠。 (当然,真正的fold函数不会把数组吐成多个数组。)一个线程执行Array(1,2).fold(0)(_ + _),计算0 + 1 + 2。另一个线程执行Array(3,4).fold(0)(_ + _),计算0 + 3 + 4。最后,将两个线程的部分和相加。请注意,中性元素0 出现了两次。

    【讨论】:

    • fold 相对于foldleft 的优势在于fold 可以并行处理数据,而foldLeft 是线性的,因此fold 可以比foldLeft 更快。对吗?
    【解决方案3】:

    注意:我在这里可能完全错了。我的 scala 不够完美。

    我认为区别在于方法的签名:

    def fold[A1 >: A](z: A1)(op: (A1, A1) ⇒ A1): A1
    

    def foldLeft[B](z: B)(op: (B, T) ⇒ B): B
    

    简而言之,折叠被定义为对某个类型 A1 进行操作,它是数组类型的超类型,对于您的字符串数组,编译器将其定义为“Any”(可能是因为它需要一种可以存储您的 String 一个int-通知传递给 fold Fold 的组合器方法需要两个相同类型的参数?)这也是文档在谈论 z 时的意思- Fold 的实现可能是这样的,它结合了您的并行输入,例如:

    "1" + "2" --\
                 --> 3 + 3 -> 6
    "3" + *z* --/
    

    另一方面, foldLeft 对 B 类型(无约束)进行操作,只要求您提供一个组合器方法,该方法采用 B 类型的参数和数组类型的另一个参数(在您的情况下为字符串),并生成 B .

    【讨论】:

    • 这几乎是完美的。 A1A 的超类型——也就是说,我可以从 Int 转到 Any(例如),但不能从 Any 转到 Int
    • 好的。在这种情况下,我不确定为什么它被转换为Any 而不是AnyVal。但除此之外,我想我明白了。
    • @KarelBílek 大概是因为它需要 String 和 int 共有的基本类型。 String 是(再次假设,因为它是在 Java 中)一个引用类型。
    【解决方案4】:

    错误。你得到一个编译时错误,因为 fold 的签名只允许折叠类型的值,它是集合中值的类型的超类型,并且String(你的集合类型)和Int(你提供的零元素的类型)的唯一超类型是Any。因此,折叠结果的类型被推断为Any - 而Any 没有方法toInt

    注意fold的两个版本有不同的签名:

    fold[A1 >: A](z: A1)(op: (A1, A1) => A1): A1
    
    foldLeft[B](z: B)(f: (B, A) => B): B
    

    为什么他们有不同的签名?这是因为fold 可以并行实现,就像并行集合的情况一样。当多个处理器对集合中的值进行折叠时,每个处理器都采用A 类型元素的子集,并通过连续应用op 生成A1 类型的折叠值。这些处理器产生的结果必须组合成一个最终的折叠值 - 这是使用 op 函数完成的,它就是这样做的。

    现在,请注意,这不能使用foldLeft 中的f 来完成,因为每个处理器都会产生B 类型的折叠值。 B 类型的多个值不能使用f 组合,因为f 仅将值B 与另一个A 类型的值组合-AB 类型之间没有对应关系。

    示例。 在您的示例中,假设第一个处理器采用元素 "1", "2",第二个处理器采用元素 "3"。第一个将产生折叠值3,第二个将产生另一个折叠值3。现在他们必须结合他们的结果来获得最终的折叠值——这是不可能的,因为闭包 _ + _.toInt 只知道如何结合 IntString,而不是 2 个 Int 值。

    对于这些类型不同的情况,请使用aggregate,其中您必须定义如何组合B 类型的两个值:

    def aggregate[B](z: B)(seqop: (B, A) => B, combop: (B, B) => B): B
    

    上面的combop定义了当折叠结果和集合中的元素有不同类型时如何做最后一步。

    中性元素。 如上所述,多个处理器可以折叠集合中的元素子集。它们中的每一个都将通过添加中性元素来开始其折叠值。

    在以下示例中:

    List(1, 2, 3).foldLeft(4)(_ + _)
    

    总是返回10 = 4 + 1 + 2 + 3

    但是,4 不应与 fold 一起使用,因为它不是中性元素:

    List(1, 2, 3).fold(4)(_ + _)
    

    以上可能返回(4 + 1 + 2) + (4 + 3) = 14(4 + 1) + (4 + 2) + (4 + 3) = 18。如果您不对fold 使用中性元素,则结果是不确定的。同理,您可以将Nil 用作中性元素,但不能用作非空列表。

    【讨论】:

      【解决方案5】:

      正如另一个答案指出的那样,fold 方法主要用于支持并行折叠。您可以看到如下。首先,我们可以为整数定义一种包装器,它允许我们跟踪对其实例执行的操作。

      case class TrackInt(v: Int) {
        val log = collection.mutable.Buffer.empty[Int]
        def plus(that: TrackInt) = {
          this.log += that.v
          that.log += this.v
          new TrackInt(this.v + that.v)
        }
      }
      

      接下来我们可以创建这些东西的并行集合和一个标识元素:

      val xs = (1 to 10).map(TrackInt(_)).par
      val zero = TrackInt(0)
      

      首先我们试试foldLeft:

      scala> xs.foldLeft(zero)(_ plus _)
      res0: TrackInt = TrackInt(55)
      
      scala> zero.log
      res1: scala.collection.mutable.Buffer[Int] = ArrayBuffer(1)
      

      所以我们的零值只使用一次,正如我们所期望的那样,因为foldLeft 执行顺序折叠。接下来我们可以清空日志试试fold

      scala> zero.log.clear()
      
      scala> xs.fold(zero)(_ plus _)
      res2: TrackInt = TrackInt(55)
      
      scala> zero.log
      res3: scala.collection.mutable.Buffer[Int] = ArrayBuffer(1, 6, 2, 7, 8)
      

      所以我们可以看到折叠已经以这样的方式并行化,即多次使用零值。如果我们再次运行它,我们可能会在日志中看到不同的值。

      【讨论】:

        【解决方案6】:

        一般区别

        这里是方法的原型

        fold[A1 >: A](z: A1)(op: (A1, A1) ⇒ A1): A1
        foldLeft[B](z: B)(f: (B, A) ⇒ B): B
        

        因此,对于 fold,结果的类型是 A1 &gt;: A,而不是任何 B。此外,如文档中所述,fold 的顺序不是

        关于您的错误

        输入scala&gt; Array("1","2","3").fold(0)(_ + _.toInt) 时,您假定0intString 的子类型。这就是编译器抛出错误的原因。

        关于折叠中奇怪的 z

        在这里,我们必须查看foldimplementation 才能了解发生了什么。这是我们得到的:

        def fold[A1 >: A](z: A1)(op: (A1, A1) => A1): A1 = foldLeft(z)(op)
        

        所以基本上,foldfoldleft 的实现,对输出类型有限制。 我们现在可以看到z 在实践中的使用方式与foldleft 相同。因此,我们可以得出结论,之所以做出此评论,是因为在未来的实现中没有任何东西可以保证这种行为。我们现在已经可以看到它了,parallels:

        def fold[U >: T](z: U)(op: (U, U) => U): U = {
          executeAndWaitResult(new Fold(z, op, splitter))
        }
        

        【讨论】:

          【解决方案7】:

          已经提到,但没有例子:如果你想允许输出和输入的不同数据类型的并行性,你可以使用aggregate

          Array("1","2","3").aggregate(0)(_ + _.toInt, _ + _)
          

          第一个函数首先被调用。然后用第二个函数减少它的结果。见Explanation of the aggregate scala function

          【讨论】: