我使用last
和foldl1
撰写了foldr1
函数。
lastr :: [a] -> a
lastr = foldr1 (flip const)
lastl :: [a] -> a
lastl = foldl1 (flip const)
它们只适用于短名单。但是当我尝试使用很长的列表[1..10 ^ 8]时,lastr在6.94秒内返回解决方案,但是lastl内存不足。
foldr1和foldl1的定义是(根据我的理解)
foldr1 f [x] = x
foldr1 f (x:xs) = f x $ foldr1 f xs
和
foldl1 f [x] = x
foldl1 f (x:y:ys)=foldl1 f $ f x y : ys
从这些看来,foldl1似乎比foldr1使用更少的内存,因为foldr1需要保持像f x1 $ f x2 $ f x3 $ f x4 $...
这样的表达式,而foldl1每次只能计算f x y
并将其存储为列表的头元素而不是持有它直到达到10 ^ 8。
有谁能告诉我我的论点有什么问题?
答案 0 :(得分:19)
如果组合函数在其第二个参数中是惰性的,则右侧折叠可以立即开始生成。一个简单的例子:
foldr1 (++) ["one", "two", "three", ...]
~> "one" ++ foldr1 (++) ["two", "three", ...]
并且可以立即访问结果的第一部分,而无需进一步评估(++)
的第二个参数。只需在消耗第一部分时评估。通常,第一部分可能已经被垃圾收集。
在f = flip const
作为组合函数的示例中,我们有不同的情况,即第二个参数中的严格(1),但不需要在所有。它忽略了它的第一个。这对右折也有好处。在这里
foldr1 f [x1, x2, x3, ... ]
~> f x1 (foldr1 f [x2, x3, ... ])
现在可以立即评估最外面的f
~> foldr1 f [x2, x3, ... ]
~> f x2 (foldr1 f [x3, ... ])
~> foldr1 f [x3, ... ]
并且在每一步中,最外面的f
总是可以立即被评估(完全),并且一个列表元素被丢弃。
如果列表是由生成器给出的,可以在按顺序使用时在恒定空间中创建它,
last = foldr1 (flip const)
可以在恒定的空间内运行。
左侧折叠,情况有所不同。因为这是尾递归的
foldl1 f (x:y:zs) = foldl f x (y:zs) = foldl f (f x y) zs
在折叠到达列表末尾之前,它无法返回任何内容。特别是,左侧折叠永远不会终止于无限列表。
现在,查看我们的案例f = flip const
,我们找到了
foldl1 f [x1, x2, x3, x4, ...]
~> foldl f x1 [x2, x3, x4, ... ]
~> foldl f (f x1 x2) [x3, x4, ... ]
~> foldl f (f (f x1 x2) x3) [x4, ... ]
当然可以立即评估f x1 x2
至x2
,然后评估f x2 x3 = x3
,但这仅适用于此特殊f
。
由于foldl
是一般的高阶函数,它不能在需要之前评估中间结果,因为中间结果可能永远不需要 - 实际上,这里从不需要它们,在列表的末尾,有一个表达式
f (f (f (f ...y3) y2) y1) y0
~> y0
然后可以在不查看构建第一个参数的大量嵌套f
的情况下评估最外层的f
。
foldl
(分别为foldl1
)无法知道立即评估中间结果会更有效率。
严格的左侧折叠,foldl'
和foldl1'
这样做,它们将中间结果评估为弱头正常形式(到最外面的值构造函数或lambda),并且
last = foldl1' (flip const)
也非常有效。
但是,由于中间结果的评估比foldr
更进一步,因此它们的效率会低一些,而且重要的是,如果任何列表元素是⊥
,则foldl1'
版本会返回⊥
:
foldl1' f [x1, ⊥, x3, x4]
~> foldl' f x1 [⊥, x3, x4]
~> case f x1 ⊥ of
pattern -- that causes ⊥
~> ⊥
而foldr1
版本没有问题,因为它根本不检查列表元素或中间结果。
(1) f
在第二个参数中是严格的意味着
f x ⊥ = ⊥
由于f
只返回其第二个参数,显然就是这种情况。