foldR Tail在Haskell中是递归的吗?
我是Haskell的新手,读Haskell的第一原理, 在第384页的折叠章节中,我遇到了FoldR,似乎它的尾部不是递归的foldR Tail在Haskell中是递归的吗?,haskell,optimization,ghc,tail-recursion,fold,Haskell,Optimization,Ghc,Tail Recursion,Fold,我是Haskell的新手,读Haskell的第一原理, 在第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-这样做会被优化吗?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-这样做会被优化吗?
foldr
不是尾部递归的。
有时它被称为真正的“递归”折叠,而左折叠是“迭代的”(因为尾部递归,它相当于迭代)
请注意,在Haskell中,由于懒惰的原因,
foldl
也不能保证常量空间,这就是为什么在Haskell这样的懒惰语言中存在foldl'
,尾部递归通常是个坏主意。这就是其中之一
我们可以尝试使foldr
尾部递归,例如从reverse
开始(以尾部递归的方式也是可行的),然后以尾部递归的方式逐步累积foldr
结果。然而,这将打破foldr
的语义
使用标准foldr
,我们有
foldr (\_ _ -> k1) k2 (x:xs) = k1
无论xs
是什么,包括底部值(如undefined
)或无限列表(如[0..]
)。此外,当xs
是一个有限但很长的列表时,上面的代码也是有效的,因为它在不扫描整个列表的情况下立即停止计算
作为一个更实际的例子
and :: [Bool] -> Bool
and = foldr (&&) True
当xs
的某些元素计算为False
时,使和xs
返回False
,而不扫描列表的其余部分
最后,将foldr
转换为尾部递归函数将:
- 在处理部分定义的列表(
)或无限列表(1:2:undefined
)时更改语义李>[0..]
- 在有限长度的列表上效率较低,即使没有必要,也必须对其进行完全扫描
foldr
不是尾部递归。。。但它可以用来编写在常量空间中处理列表的函数。迟浩田已经指出,它可以有效地实现和。下面是它如何实现一个高效的列表求和函数:
mySum :: Num a => [a] -> a
mySum xs = foldr go id xs 0
where
go x r acc = r $! x + acc
这工作怎么样?考虑<代码> MySoM[1,2,3] < /代码>。这扩展到
foldr go id [1,2,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
我们已经将列表大小减少了一个,并且没有在“堆栈”上积累任何内容。同样的过程重复,直到我们得到
foldr go id [] 6
==> definition of foldr
id 6
==> definition of id
6
注意:如果GHC编译此代码时启用了优化(-O
或-O2
),那么它实际上会将其转换为炽热的快尾递归代码,而无需您的进一步帮助。但即使未优化,它也可以正常工作,不会消耗大量内存/堆栈。foldr
本身不是尾部递归,但根据f
,可能是尾部递归。Haskell中的尾部递归非常微妙,因为它的计算很慢
例如,考虑<代码> f=(& &)。在这种情况下,我们有
foldr(&&)acc lst=
第1宗
[]->acc
(x:xs)->x&&foldr(&&&)acc xs
=
第1宗
[]->acc
(x:xs)->如果x
然后foldr(&&)acc xs
否则错误
=
第1宗
[]->acc
(x:xs)->
True->foldr(&&)acc xs
假->假
注意,在本例中,我们清楚地看到foldr(&&)
是尾部递归的。实际上,foldr(| |)
也是尾部递归的。请注意,foldr(&&)
是尾部递归的,这基本上是因为懒惰。如果不是因为懒惰,在将结果代入x&&foldr(&&&)acc xs
之前,我们必须评估foldr(&&&)acc xs
。但由于懒惰,我们首先评估x
,然后才确定是否需要调用foldr(&&&)acc xs
,无论何时调用,都是尾部调用
但是,在大多数情况下,foldr f
将不是尾部递归函数。特别是,foldr((+)::Int->Int->Int)
不是尾部递归的。你能给我fold的引用吗?@hanan'这里他们定义了foldr,使用continuation(对我来说,它就像返回一个函数一样)在haskell中使用它是否经过优化?或者它也会破坏语义?@hanan你认为“优化”是什么意思?@hanan你在比较苹果和橙子。我的答案考虑了像Haskell这样的懒惰语言,而你提到的答案是关于F#,它是渴望的,所以语义是不同的。由于这种差异,我们无法对两种语言应用相同的推理:一种语言中的良好实践很容易成为另一种语言中的不良实践,反之亦然。^^^^^具体来说,F#和Haskell中的这种“CPS”技术首先从n长列表构建一个depth-n闭包,然后将其应用于初始值(在您链接的答案中称为acc
)。然后,在严格的语言中,执行n个应用程序链,从下到上构建结果,这是可以的。但在惰性语言中,应用程序cont{func x r}
不会减少(即计算)应用程序func x r
——相反,它将按原样传递到cont
。其效果是另外两个无关的遍历,其中一个在堆栈上,可能会导致堆栈溢出…@hanan…补救方法是使用(\r->cont$!func x r)
作为每一步的延续,以获得所需的严格性。@Hana不知道为什么人们会投反对票,因为这是一个完全合理的问题。但是,正如答案正确指出的那样,“尾部递归”在Haskell中并不是真的那么重要。你更担心thunks的存储空间(这就是为什么foldl
客观上比foldl'
更糟糕的一个重要原因,例如,尽管可以以简单的方式递归实现这两个尾部。Don