我有以下基本的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
答案 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中了解有关此严格应用的更多信息。