是的,在某些情况下。这称为尾调用优化。以下面的 C-ish 阶乘函数为例:
int factorial(int n)
{
if (n < = 1)
return 1;
else
return n * factorial(n – 1);
}
此函数在“展开”备份之前深入调用堆栈以获得结果。以下是 OCaml 等价物:
let factorial n = if n <= 1 then 1 else n * factorial (n - 1)
现在实际上,上面的代码也将深入调用堆栈 n 层,就像上面的 C-ish 代码一样。这是完成相同但带有循环的 C-ish 函数:
int factorial(int n)
{
int ret = 1;
for (; n > 1; n--)
ret *= n;
return ret;
}
当然,这个函数可以被调用任意次数而不会溢出堆栈(即使你很快就会溢出一个 32 位的 int)。实际上可以在 OCaml 中编写同义函数。现在,OCaml 的版本将再次使用递归。但是,如果我们在第一个函数中添加一个“累加器”参数,它可以重写为:
let factorial acc n = if n <= 1 then acc else factorial (acc * n) (n – 1)
acc 参数可以被认为是“累积”所有先前对阶乘的递归调用的结果。关键效果是上面的表达式“n * factorial (n -1)”变成了下面的表达式“factorial (acc * n) (n – 1)”。在第二个表达式中,对阶乘的递归调用是表达式的顶层,这意味着不需要对其执行额外的操作来获取函数的返回值。这不适用于第一个表达式,其中顶级运算是 n – 1 的阶乘结果与 n 的乘积。当递归函数调用是顶级表达式时,它被认为是“尾调用”,编译器可以并将其优化为有效的循环。在第一个函数上调用“factorial 2000000”将(可能)导致堆栈溢出,但在第二个函数上调用“factorial 1 2000000”不会。此外,您可能会发现第二个 OCaml 函数在性能方面与 C 等效函数相当(它可能会慢一点,但不是数量级或其他任何东西)。
顺便说一句,您可能会问自己,“但尾递归函数有一个不必要的额外 'acc' 参数,在用户最初调用时应该始终为 1,这不是很麻烦吗?”是的,是的。这个问题很容易通过将尾递归函数嵌套到一个“包装器”函数中来解决,该函数使用正确的初始累积值调用它,如下所示:
let factorial n =
let loop acc n' = if n' <= 1 then acc else loop (acc * n') (n' – 1) in
loop 1 n
在这里,我将上面的尾递归阶乘函数重命名为“循环”并将其嵌套在一个函数中,然后使用正确的初始累加器 1 调用它。
通常情况下,这些尾递归模式可以用标准库中的高阶函数代替,例如 List.fold,但并非总是如此。