Haskell 尾递归函数的效率
Efficiency of Haskell Tail-recursive functions
我有以下基本 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
让我们以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
直到 sum 0
被评估,其余的函数不能从内存中退出,因为它们都在等待对 return 的递归调用,以便它们可以 return 一个值。在这种情况下,它只是 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
return 的调用是调用另一个函数的结果。因此,当前函数不必在内存中,因为它的 return 值不依赖于递归调用的结果(它不依赖于递归调用的结果,而是依赖于当前函数的 return 值与递归函数调用的return值的结果相同。
编译器通常会优化循环的尾部调用,因此它们非常高效。
阅读更多关于 Tail Recursion in this wiki page
正如 Carl 在评论中提到的,理解 $!
的作用很重要。它将功能的正常应用强制为功能的严格应用。这是什么意思? $!
基本上将表达式简化为 Head 范式。什么是头部范式?表达式将被简化,直到它成为函数应用程序或数据构造函数。
考虑一下
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)
Haskell 在实际调用 func
之前不会计算 1 - 1
。因此,在调用 func
之后,它将计算表达式并发现它是 0
并且它将选择 func 0 = 100
和 return 100
。但是我们可以强制严格应用,像这样
func $! (1 - 1)
现在,haskell 将首先计算 (1 - 1)
,然后它会知道该值为 0
。因此,它将直接调用 func 0 = 100
和 return 100
。我们通过强制严格应用来减轻编译器的负担。
您可以在 this haskell wiki page.
中阅读有关此严格应用程序的更多信息
我有以下基本 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
让我们以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
直到 sum 0
被评估,其余的函数不能从内存中退出,因为它们都在等待对 return 的递归调用,以便它们可以 return 一个值。在这种情况下,它只是 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
return 的调用是调用另一个函数的结果。因此,当前函数不必在内存中,因为它的 return 值不依赖于递归调用的结果(它不依赖于递归调用的结果,而是依赖于当前函数的 return 值与递归函数调用的return值的结果相同。
编译器通常会优化循环的尾部调用,因此它们非常高效。
阅读更多关于 Tail Recursion in this wiki page
正如 Carl 在评论中提到的,理解 $!
的作用很重要。它将功能的正常应用强制为功能的严格应用。这是什么意思? $!
基本上将表达式简化为 Head 范式。什么是头部范式?表达式将被简化,直到它成为函数应用程序或数据构造函数。
考虑一下
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)
Haskell 在实际调用 func
之前不会计算 1 - 1
。因此,在调用 func
之后,它将计算表达式并发现它是 0
并且它将选择 func 0 = 100
和 return 100
。但是我们可以强制严格应用,像这样
func $! (1 - 1)
现在,haskell 将首先计算 (1 - 1)
,然后它会知道该值为 0
。因此,它将直接调用 func 0 = 100
和 return 100
。我们通过强制严格应用来减轻编译器的负担。
您可以在 this haskell wiki page.
中阅读有关此严格应用程序的更多信息