问题描述
我是 Haskell 的新手,从基本原理开始阅读 Haskell, 在 Folds 第 384 页中,我遇到了 FoldR 并且它似乎不是尾递归
foldr :: (a -> b -> b) -> b -> [a] -> b
foldr f z [] = z
foldr f z (x:xs) = f x (foldr f z xs)
1- 我们可以让它尾递归吗?
2-它会被优化吗?
解决方法
在像 Haskell 这样的懒惰语言中,尾递归通常是一个坏主意。这是其中一种情况。
我们可能会尝试通过例如 foldr
尾递归从 reverse
开始(也可以用尾递归方式),然后以尾递归方式逐步累积 foldr
结果。但是,这会破坏 foldr
语义。
使用标准 foldr
,我们有例如
foldr (\_ _ -> k1) k2 (x:xs) = k1
无论 xs
是什么,包括像 undefined
这样的底值或像 [0..]
这样的无限列表。此外,当 xs
是一个有限但很长的列表时,上面的代码也是有效的,因为它会立即停止计算而无需扫描整个列表。
举一个更实际的例子,
and :: [Bool] -> Bool
and = foldr (&&) True
只要 and xs
的某个元素的计算结果为 False
,就让 xs
返回 False
,而不扫描列表的其余部分。
最后,将 foldr
转换为尾递归函数将:
- 在处理部分定义的列表 (
1:2:undefined
) 或无限列表 ([0..]
) 时更改语义; - 在有限长度列表上效率较低,即使不需要,也总是必须完全扫描。
foldr
不是尾递归......但它可用于编写在常量空间中处理列表的函数。 chi 已经指出它可以有效地实现 and
。以下是它如何实现对列表求和的有效函数:
mySum :: Num a => [a] -> a
mySum xs = foldr go id xs 0
where
go x r acc = r $! x + acc
效果如何?考虑mySum [1,2,3]
。这扩展为
foldr go id [1,3] 0
==> -- definition of foldr
go 1 (foldr go id [2,3]) 0
==> -- definition of go
foldr go id [2,3] $! 1 + 0
==> -- strict application
foldr go id [2,3] 1
我们将列表大小减少了 1,并且没有在“堆栈”上累积任何内容。重复相同的过程,直到我们得到
foldr go id [] 6
==> definition of foldr
id 6
==> definition of id
6
注意:如果此代码由 GHC 编译并启用优化(-O
或 -O2
),那么它实际上会将其转换为极快的尾递归代码,而无需您的任何进一步帮助。但即使未优化,它也能正常工作,不会烧掉一堆内存/堆栈。
struct ContentView: View {
var body: some View {
VStack(spacing: 4.0) {
ForEach(1..<4) {
Text(Array<String>(repeating: "word",count: $0).joined(separator: " "))
.frame(maxWidth: .infinity) /// keep the maxWidth
.background(Color.gray)
}
}
.fixedSize() /// here!
}
}
不是尾递归。
有时它被称为真正的“递归”折叠,而向左折叠是“迭代”(因为尾递归相当于迭代)。
请注意,在 Haskell 中,由于懒惰,foldr
也不保证空间恒定,这就是它存在的原因 foldl