【问题标题】:foldLeft v. foldRight - does it matter?foldLeft v. foldRight - 有关系吗?
【发布时间】:2014-06-23 16:11:02
【问题描述】:

之前,Nicolas Rinaudo 在 Scala 的 List foldRight Always Using foldLeft? 上回答了我的问题

目前正在学习Haskell,我的理解是foldRight 应该优先于foldLeft,在::(前置)可以使用++(附加)的情况下。

据我了解,原因是性能-前者发生在O(1),即在前面添加一个项目-恒定时间。而后者需要O(N),即遍历整个列表并添加一个项目。

在 Scala 中,鉴于foldLeft 是根据foldRight 实现的,使用:+ 是否比++foldRight 更重要,因为foldRight 被反转,然后foldLeft'd ?

例如,考虑这个简单的fold.. 操作,它只是按顺序返回列表的元素。

foldLeft 折叠每个元素,通过:+ 将每个项目添加到列表中。

scala> List("foo", "bar").foldLeft(List[String]()) { 
                                                    (acc, elem) => acc :+ elem }
res9: List[String] = List(foo, bar)

foldRight 对每个项目执行带有 :: 运算符的 foldLeft,但随后反转。

scala> List("foo", "bar").foldRight(List[String]()) { 
                                                    (elem, acc) => elem :: acc }
res10: List[String] = List(foo, bar)

实际上,鉴于foldRight 使用foldRight,在Scala 中使用foldLeftfoldRight 是否重要?

【问题讨论】:

标签: scala haskell


【解决方案1】:

@Rein Henrichs 的回答确实与 Scala 无关,因为 Scala 对 foldLeftfoldRight 的实现完全不同(对于初学者来说,Scala 有热切的评估)。

foldLeftfoldRight 本身实际上对程序的性能几乎没有什么可做的。两者都是(从广义上讲) O(n*c_f) 其中 c_f 是对给定函数f 的一次调用的复杂度。不过,foldRight 的速度要慢一个常数因子,因为有额外的 reverse

因此,区分两者的真正因素是您提供的匿名函数的复杂性。有时,编写一个与foldLeft 一起使用的高效函数更容易,有时与foldRight 一起使用会更容易。在您的示例中,foldRight 版本是最好的,因为您提供给 foldRight 的匿名函数是 O(1)。相比之下,您提供给 foldLeft 的匿名函数本身就是 O(n)(摊销,这在这里很重要),因为 acc 不断从 0 增长到 n-1,并附加到 n 个元素的列表是 O(n)。

所以实际上很重要你是选择foldLeft 还是foldRight,但这不是因为这些函数本身,而是因为赋予它们的匿名函数。如果两者相等,则默认选择foldLeft

【讨论】:

  • 如果函数是非关联的,我认为它在 Scala 中也很重要?
  • 嗯,是的,很明显。鉴于问题的表述,我认为这很清楚。
  • 这似乎是合理的,因为我做了同样的事情。 ;)
  • 谢谢,@sjrd。根据 Nicolas 对我的问题的comment,我不确定这个答案在 2.12 中是否仍然正确,因为我从这个源代码链接中了解到,foldRight 不再根据foldLeft 实现?
  • foldRight 在 2.12.0-M3 中仍以 foldLeft 实现:github.com/scala/scala/blob/…
【解决方案2】:

我可以为 Haskell 提供答案,但我怀疑它是否与 Scala 相关:

让我们从两者的来源开始,

foldl f z []     = z
foldl f z (x:xs) = foldl f (f z x) xs

foldr f z []     = z
foldr f z (x:xs) = f x (foldr f z xs)

现在,让我们看看对 foldl 或 foldr 的递归调用出现在右侧的什么位置。对于 foldl,它位于最外层。对于 foldr,它位于 f 的应用程序内部。这有几个重要的含义:

  1. 如果f 是一个数据构造函数,则该数据构造函数将位于最左边,最外面的foldr。这意味着 foldr 实现了guarded recursion,因此以下是可能的:

    > take 5 . foldr (:) [] $ [1..]
    [1,2,3,4]
    

    这意味着,例如,对于 short-cut fusion,foldr 既可以是好的生产者,也可以是好的消费者。 (是的,foldr (:) [] 是列表的恒等态射。)

    这对于 foldl 是不可能的,因为构造函数将在对 foldl 的递归调用中并且无法进行模式匹配。

  2. 相反,由于对 foldl 的递归调用位于最左、最外的位置,它会被惰性求值减少,并且不会占用模式匹配堆栈上的空间。结合适当的严格性注释(例如,foldl'),这允许像 sumlength 这样的函数在恒定空间中运行。

有关更多信息,请参阅Lazy Evaluation of Haskell

【讨论】:

  • 我真的很喜欢这两者的描述!
  • 非常感谢,@J.Abrahamson!
  • tl;dr:使用 foldr 进行惰性处理,foldl' 进行严格处理。
【解决方案3】:

实际上,在 Scala 中使用 foldLeft 还是 foldRight 确实很重要,至少在列表中,至少在默认实现中是这样。不过,我相信这个答案不适用于 scalaz 等库。

如果你查看foldLeftfoldRight 的源代码LinearSeqOptimized,你会看到:

  • foldLeft 使用循环和局部可变变量实现,适合一个堆栈帧。
  • foldRight 是递归的,但不是尾递归的,因此列表中的每个元素消耗一个堆栈帧。

foldLeft 因此是安全的,而foldRight 可能会因长列表而堆栈溢出。

编辑 为了完成我的回答,因为它只解决了您的部分问题:您使用哪一个也很重要,具体取决于您打算做什么。

以您为例,我认为最佳解决方案是使用foldLeft,将结果添加到您的累加器,然后将结果添加到reverse

这边:

  • 整个操作是O(n)
  • 无论列表大小,它都不会溢出堆栈

这实质上就是你认为你在用foldRight 做的事情,假设它是根据foldLeft 实现的。

如果您使用foldRight,您会以安全为代价获得稍快的实现(嗯,稍微...快两倍,真的,但仍然是 O(n))。

有人可能会争辩说,如果您知道您的列表足够小,就不存在安全问题,您可以使用foldRight。我觉得,但这只是一种观点,如果你的列表足够小,你不必担心你的堆栈,它们也足够小,你也不必担心性能损失。

【讨论】:

    【解决方案4】:

    这取决于,考虑以下几点:

    scala> val l = List(1, 2, 3)
    l: List[Int] = List(1, 2, 3)
    
    scala> l.foldLeft(List.empty[Int]) { (acc, ele) => ele :: acc }
    res0: List[Int] = List(3, 2, 1)
    
    scala> l.foldRight(List.empty[Int]) { (ele, acc) => ele :: acc }
    res1: List[Int] = List(1, 2, 3)
    

    如您所见,foldLeft 遍历列表从head 到最后一个元素。另一方面, foldRight 将其从最后一个元素遍历到 head

    如果你使用折叠进行聚合,应该没有区别:

    scala> l.foldLeft(0) { (acc, ele) => ele + acc }
    res2: Int = 6
    
    scala> l.foldRight(0) { (ele, acc) => ele + acc }
    res3: Int = 6
    

    【讨论】:

      【解决方案5】:
      scala> val words = List("Hic", "Est", "Index")
      words: List[String] = List(Hic, Est, Index)
      

      如果是 foldLeft: 列表元素会先添加到空字符串,然后再添加

      words.foldLeft("")(_ + _) == (("" + "Hic") + "Est") + "Index"      //"HicEstIndex"
      

      如果是 foldRight: 空字符串将添加到元素的末尾

      words.foldRight("")(_ + _) == "Hic" + ("Est" + ("Index" + ""))     //"HicEstIndex"
      

      两种情况都会返回相同的输出

      def foldRight[B](z: B)(f: (A, B) => B): B
      def foldLeft[B](z: B)(f: (B, A) => B): B
      

      【讨论】:

        【解决方案6】:

        我不是 Scala 专家,但在 Haskell 中,foldl'(实际上应该是默认的左折叠)和foldr 之间最重要的区别之一是foldr 将适用于无限数据结构,其中foldl' 将无限期挂起。

        为了理解为什么会这样,我建议访问foldl.comfoldr.com,将评估扩展几次,并重建调用树。您会很快看到 foldrfoldl' 的合适位置。

        【讨论】:

          猜你喜欢
          • 2017-03-25
          • 2017-11-04
          • 2013-06-12
          • 1970-01-01
          • 1970-01-01
          • 2011-09-09
          • 1970-01-01
          • 1970-01-01
          • 2023-03-30
          相关资源
          最近更新 更多