foldl (\b a -> a ++ "-----\n" ++ b) "" (x : xs)
==> foldl (\b a -> a ++ "-----\n" ++ b) (x ++ "-----\n" ++ "") xs
型 它扩展到另一个对foldl的调用,在我们知道什么元素在结果列表的头部之前,我们必须评估它。(如果xs不为空)将返回 another 调用foldl。它将一直这样做,直到我们达到基本情况,并最终返回++应用程序的大链。所以我们不能得到直到我们遍历了整个字符串的原始列表。 如果我们看一下正确的fold版本,我们会看到一些不同的东西。展开foldr递归的一级(假设xs不为空),我们会得到:
foldr (\a b -> a ++ "-----\n" ++ b) "" (x : xs)
==> x ++ "-----\n" ++ foldr (\a b -> a ++ "-----\n" ++ b) "" xs)
1条答案
按热度按时间yqlxgs2m1#
foldr
在这里更好,因为++
(以及一般的列表)是“自然”右关联的。++
的定义是:字符串
注意,第二个参数
ys
在两个等式中都是未经修改地使用的,而第一个列表是分开的,它的元素被前置到正在构造的新列表的前面。所以出现在++
的 * 左边 * 的东西必须被扫描和重建,而右边出现的东西是免费的。即使我们完全展开++
的递归定义,比如:型
然后我们得到
x1 : x2 : x3 : x4 : ys
;ys
只是原样存储在数据构造函数(嵌套最深的:
)的字段中,因此不会被++
检查。但是整个xs
列表必须被遍历并重建到ys
的前面。因此,当我们折叠以构建列表时,我们希望确保累积参数(随着我们处理列表的更多内容,它会变得越来越大)始终用作
++
的 right 参数。这样就不需要在折叠的每一层都重新遍历它。所以
foldr (\a b -> a ++ "-----\n" ++ b) "" xs
是好的;b
参数接收折叠列表其余部分的结果,它被用作++
的右参数,在那里它根本不需要被检查。这意味着折叠的每一层的成本是遍历a
的成本和遍历"-----\n"
的成本。总的来说,成本是整个fold的值是所有a
值的成本之和(因此是xs
中列表的长度之和),加上一些小的固定字符串,这些字符串等于xs
中的元素数量。如果我们使用像
foldl (\b a -> b ++ "-----\n" ++ a) "" xs
这样的左折叠,情况会更糟。(a
)在++
的右边,并在左边使用前面的结果b
。所以我们保持a
不变,并且必须遍历b
以将其所有元素前置到a
的前面。这在折叠的底部很好,其中b
是""
的起始值。但是在b
之外的水平上,* 结果 * 在第一个a
字符串中折叠(加上"-----\n"
分隔符字符串),所以我们无论如何都要遍历a
字符串。然后再一次向外一级b
将是++
将另一个字符串连接到前面的结果,所以我们必须遍历新的字符串 * 加上 * 再次遍历第一个字符串 *。以这种方式折叠整个N个元素的列表,我们最终遍历第一个字符串N次,第二个N-1次,这比使用正确的折叠要昂贵得多。你可能会想到,你可以使用左折叠,但仍然把累加参数放在
++
的右边(只要你不介意最后一个字符串的顺序颠倒)。所以:型
在性能方面,这并不像非反向左折叠那样糟糕。在每一层,我们只需要遍历来自列表的传入元素,而不检查折叠列表其余部分的结果,就像右折叠一样。(这是所有左折叠所固有的,无论你使用什么函数),扩展
foldl
递归的一个级别(假设xs
不为空)会给我们:型
它扩展到另一个对
foldl
的调用,在我们知道什么元素在结果列表的头部之前,我们必须评估它。(如果xs
不为空)将返回 another 调用foldl
。它将一直这样做,直到我们达到基本情况,并最终返回++
应用程序的大链。所以我们不能得到直到我们遍历了整个字符串的原始列表。如果我们看一下正确的fold版本,我们会看到一些不同的东西。展开
foldr
递归的一级(假设xs
不为空),我们会得到:型
结果表达式不是对
foldr
的调用,而是对++
的调用(左边的是最外面的东西在这里被称为;剩下的都在++
的右参数中)。我们只需要扩展++
一步,就可以确定最终折叠列表的第一个元素是x
的第一个字符(这是原始字符串列表中的第一个字符串)。我们可以做到这一点,而不必遍历xs
字符串列表的任何其余部分,甚至处理"-----\n"
分隔符 * 一次 *。foldr
的这种“流”行为(我们可以在只检查输入列表的第一部分后获得其输出的第一部分)使foldr
能够处理foldl
根本无法处理的无限列表,如果我们最终使用像take
这样的函数,它不检查所有的结果列表,我们就可以保存很多工作 。遍历整个输入列表。但即使在最终读取整个结果的情况下,(因此无论如何都将遍历整个输入),这种方式通常还是更快。它允许元素一次处理一个,一直通过类似的管道的多个阶段处理。如果输入列表的初始生产者和输出列表的最终消费者也以这种“流”方式工作,这节省了必须一次将整个列表存储在内存中,这可以节省保存大量的时间分配和垃圾回收的内存。也有优化(例如重写规则)应用于许多标准列表函数,这些标准列表函数通常可以完全优化中间列表的:
单元格的创建,这节省了更多的时间。最终,列表是“自然”右关联的,因为它们在
:
单元格中的定义是在右边递归的。如果可以的话,以右关联的方式构建和处理它们总是更好。主要的例外是当你把一个列表处理成一个非常小的(就内存大小而言)没有任何可以进行模式匹配的子结构的值,比如
Int
或Double
。在这种情况下,不可能以流的方式进行该操作;如果你得到一个列表的长度作为一个Int
有没有“第一部分”的Int
, 可能 * 后,遍历列表的第一部分可用。Int
是全或-在这种情况下,foldl'
(注意'
后缀是strictleft fold)通常更快,因为right fold会建立一个不能被懒惰地消耗的大操作链;严格的left fold可以执行它们。