haskell中的累加器

时间:2010-11-26 03:39:10

标签: haskell tail-recursion

在Haskell,如果我写

 fac n = facRec n 1
   where facRec 0 acc = acc
         facRec n acc = facRec (n-1) (acc*n)

用GHC编译它,结果会与我使用

时的结果不同
 fac 0 = 1
 fac n = n * fac (n-1)

我可以很容易地做fac n = product [1..n]并避免整个事情,但我对如何尝试使用懒惰语言进行尾递归感兴趣。我知道我仍然可以获得堆栈溢出,因为thunks正在构建,但是当我使用累加器时,实际发生的事情实际上是不同的(就最终的编译程序而言)而不是我刚才说出的天真递归?除了提高可读性之外,遗漏尾递归是否有任何好处?如果我使用runhaskell来运行计算而不是先编译它,答案是否会发生变化?

3 个答案:

答案 0 :(得分:8)

如果您的累加器是严格的,

尾递归 在(GHC)Haskell中有意义。为了证明这个问题,这里是fac的尾递归定义的“跟踪”:

   fac 4
~> facRec 4 1
~> facRec 3 (1*4)
~> facRec 2 ((1*4)*3)
~> facRec 1 (((1*4)*3)*2)
~> facRec 0 ((((1*4)*3)*2)*1)
~> (((1*4)*3)*2) * 1
  ~> ((1*4)*3) * 2
    ~> (1*4) * 3
      ~> 1*4
    ~> 4 * 3
  ~> 12 * 2
~> 24 * 1
~> 24

缩进级别(大致)对应于堆栈级别。请注意,累加器仅在最后评估,这可能导致堆栈溢出。当然,诀窍是使累加器严格。理论上可以证明facRec是严格的,如果在严格的上下文中调用它,但我不知道有任何编译器这样做,ATM。但是,GHC 执行尾调用优化,因此facRec调用使用常量堆栈空间。

出于同样的原因foldl'通常优先于foldl,因为前者在累加器中是严格的。

关于你的第二部分,runhaskell / runghc只是GHCi的一个包装器。如果GHCi找到编译的代码,它将使用它,否则它将使用执行少量优化的字节码解释器,因此不要指望任何奇特的优化发生。

答案 1 :(得分:3)

在haskell中,如果你的累加器是严格的并且需要整个结果,那么只能以尾递归的方式编写你的程序。

使用ghc的runHaskell,程序不会被优化,因此不会进行严格性分析,因此您可能会堆栈溢出;而如果使用优化进行编译,编译器可能会检测到累加器需要严格并相应地进行优化。

要了解事情是如何以不同方式发生的,最好的方法是检查生成的核心语言a good blog post from Don Stewart explains things。顺便说一下,如果你对性能感兴趣,他的许多博文都很有趣。

答案 2 :(得分:1)

您的问题尚不完整。我假设你的意思是GHC,并且至少没有优化,答案是肯定的,因为工作者函数(第一个中的facRec或第二个中的fac)与一个和组件相比具有一个arity 2会反映这一点。通过优化或使用JHC,答案可能是“不”。