我是haskell的新手(第一次尝试fn编程),只是尝试了来自"现实世界haskell"书。有人可以纠正我,并告诉以下函数是否是尾部调用优化?如果是,那么你可以纠正我怎么做?我在递归函数中加1,所以我认为这应该抛出stackoverflow异常?
我尝试调用myLength [1..2000000],但它没有抛出任何stackoverflow异常
myLength (x:xs) = 1 + myLength(xs)
myLength [] = 0
答案 0 :(得分:7)
GHC可能会将此优化为尾调用递归,但您编写它的方式不是。为了进行尾调用递归,树中的“top”函数必须是递归调用。当我使用[1..20000000000000]
在GHCi中运行代码时,它会因内存不足而出现分段错误,因此它不适用于非常大的输入。
如果我们稍微重新安排你的定义,我认为它会更清楚地说明为什么它不是TCR:
myLength (x:xs) =
(+)
1
(myLength xs)
myLength [] = 0
在这里,我将其分解为本质上是一棵树,并且可以表示为
(+)
/ \
/ \
1 myLength
\
xs
正如您所看到的,此树中要调用的最后一个函数是(+)
,而不是myLength
。要解决此问题,您可以使用折叠模式:
myLength = go 0
where
go acc (x:xs) = go (1 + acc) xs
go acc [] = acc
现在树看起来像
go
/ \
(+) xs
/ \
1 acc
因此树中的top函数是go
,这是递归调用。或者,您可以使用内置折叠来实现此功能。为了避免累积大量的内容,我的建议是使用foldl'
中的Data.List
:
myLength = foldl' (\acc x -> acc + 1) 0
虽然这可能需要很长时间才能执行,但它不会炸毁堆栈,也不会构成占用内存的thunk。
但正如@DonStewart所说,编译器在优化过程中可以自由地重新安排代码。
答案 1 :(得分:4)
天真地,这使用了堆栈。但是该语言并没有具体说明操作语义,因此编译器可以自由地重新排序,只要它不会改变观察到的严格性。
答案 2 :(得分:1)
不,这不是一个尾调用函数(非空列表中的最后一个调用将是+
,但这在Haskell中并不是一个大问题(有关详细信息,请参阅here )。
所以你不需要重组它,但如果你想要你可以用累加器来做它:
myLength :: [a] -> Int
myLength = myLength' 0
where myLength' acc [] = acc
myLength' acc (_:xs) = myLength' (acc+1) xs
如果我们想让Carl(更多一点)感到高兴,我们可以摆脱一些thunk(不会过多地复杂化 - 或者我认为,当谈到Haskell的懒惰时,我不是那么好,对不起) :
myLength :: [a] -> Int
myLength = myLength' 0
where myLength' acc [] = acc
myLength' acc xs = let acc' = acc+1
in seq acc' $ myLength' acc' (tail xs)
答案 3 :(得分:0)
在GHC中自动删除尾部呼叫(你不会提及你正在使用的编译器,所以我假设最常见的)因为GHC使用的呼叫约定。但是你必须非常警惕实际上 的尾部调用。在您的定义中,尾调用是(+)
。
据我所知,GHC没有任何优化可以将其改为更节省内存的形式,我怀疑@Ferruccio的评论是针对某些事情。