Haskell尾递归函数的效率

时间:2015-03-21 15:47:43

标签: haskell recursion tail-recursion

我有以下基本的Haskell代码:

prodsum x = prod x + sum x 

prod 0 = 1
prod n = n * prod (n-1)

sum 0 = 0
sum n = n + sum (n-1)

任何人都可以解释为什么下面的代码更有效:

prod' n = prodTR n 1
   where prodTR 0 r = r
         prodTR n r = prodTR (n-1) $! (r*n)

sum' n = sumTR n 0
   where sumTR 0 r = r
         sumTR n r = sumTR (n-1) $! (r+n)

prodsum' n = prod' n + sum' n

1 个答案:

答案 0 :(得分:4)

让我们以sum为例。让我们说你用5

调用它
sum 5 = 5 + (sum 4)
      = 5 + (4 + sum 3)
      = 5 + (4 + (3 sum 2))
      = 5 + (4 + (3 + (2 + (sum 1))))
      = 5 + (4 + (3 + (2 + (1 + sum 0))))
      = 5 + (4 + (3 + (2 + (1 + 0))))
      = 5 + (4 + (3 + (2 + 1)))
      = 5 + (4 + (3 + 3))
      = 5 + (4 + 6)
      = 5 + 10
      = 15

Till sum 0被评估,其余的函数无法从内存中退出,因为它们都在等待递归调用返回,以便它们可以返回一个值。在这种情况下,它只有5,想象100000。

但是,sum'将像这样评估

sum' 5 = sumTR 5 0
       = sumTR 4 (0 + 5)
       = sumTR 4 5
       = sumTR 3 (5 + 4)
       = sumTR 3 9
       = sumTR 2 (9 + 3)
       = sumTR 2 12
       = sumTR 1 (12 + 2)
       = sumTR 1 14
       = sumTR 0 (1 + 14)
       = sumTR 0 15
       = sumTR 2 (9 + 3)
       = 15

这里,对sumTR的调用返回调用另一个函数的结果。因此,当前函数不必必须在内存中,因为它的返回值不依赖于递归调用的结果(它不依赖于它,但当前函数的返回值与结果相同)递归函数调用的返回值。)

编译器通常优化对循环的尾调用,因此它们非常有效。

详细了解Tail Recursion in this wiki page


正如卡尔在评论中提到的,了解$!在这里的作用非常重要。它强制将函数的正常应用强制应用于函数。这是什么意思? $!基本上将表达式缩减为Head normal格式。什么是头部正常形式?表达式将被简化,直到它成为函数应用程序或数据构造函数。

考虑一下

sumTR (n-1) $ (r+n)

这里r + n将在调用sumTR函数后对其进行评估,就像我在上面的扩展中所示。因为Haskell懒惰地评估所有内容。但是,您可以在函数调用之前强制评估r + n,并使用r + n的结果调用它。这将带来巨大的运行时优势,因为编译器不必等到调用以确定要调用的函数表达式,如果它必须进行模式匹配。例如,

func :: Int -> Int
func 0 = 100
func a = a + a

在这里,如果我调用func,就像这样

func $ (1 - 1)

在实际调用1 - 1之前,Haskell不会评估func。因此,在调用func后,它会评估表达式并找到0,然后选择func 0 = 100并返回100。但是我们可以强制严格的应用,比如这个

func $! (1 - 1)

现在,haskell将首先评估(1 - 1),然后它将知道该值为0。因此,它将直接调用func 0 = 100并返回100。我们通过强制严格应用来减轻编译器的负担。

您可以在this haskell wiki page中了解有关此严格应用的更多信息。