以下函数尾调用是否已优化?

时间:2014-08-19 14:40:33

标签: haskell recursion tail-recursion

我是haskell的新手(第一次尝试fn编程),只是尝试了来自"现实世界haskell"书。有人可以纠正我,并告诉以下函数是否是尾部调用优化?如果是,那么你可以纠正我怎么做?我在递归函数中加1,所以我认为这应该抛出stackoverflow异常?

我尝试调用myLength [1..2000000],但它没有抛出任何stackoverflow异常

myLength (x:xs) = 1 + myLength(xs) 
myLength [] = 0

4 个答案:

答案 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的评论是针对某些事情。