3.3.1 递归计算
递归函数最常见的示例是计算一个数的阶乘。如果你不熟悉,这里有一个简单的定义:一个非负数 n 的阶乘,当 n 等于 1 或 0 时,等于1;对于更大的 n,等于 n-1 的阶乘乘以 n。这个函数的实现,基本上有两种方式。在 C# 中,可以使用 for 循环,数字在 2 到 n 之间进行迭代,用第次迭代的数乘以临时变量:
int Factorial(int n) {
intres = 1;
for(int i = 2; i <= n; i++)
res = res * i;
return res;
}
这是一个正确的实现,但不容易看出函数所对应的数学定义。函数的第二种实现方式使用递归,在 C# 中定义方法,或在 F# 中定义函数,以递归方式调用自身。这两个实现惊人的相似,可以在清单 3.12 中看到并列排放的两个定义。
清单 3.12 阶乘的递归实现,用C# 和 F#
C# | F# |
int Factorial(int n) { [1] if (n <= 1) return 1; [2] else return n * Factorial(n - 1); [3] } | let rec factorial(n) = [1] if (n <= 1) then 1 [2] else n * factorial(n - 1) [3] |
递归函数或方法的声明[1]。在 F# 中,必须显式声明函数是递归的,用 let rec 绑定,代替普通的 let。
模式匹配包含两种情况。第一种情况,终止递归,并立即返回 1 [2];第二种情况,执行递归调用 factorial 函数或 Factorial 方法。
C# 版本的代码非常简单;F# 版本也相当清晰,只是必须使用 rec 关键字,显式声明函数是递归的。这个关键字说明 let 绑定是递归的,这样,就可以引用函数声明内部值的名字(factorial)。
一般情况下,每个递归计算都应有至少两个分支:一个分支用于执行递归调用的计算,另一个分支用于终止计算,在清单 3.12 中可以看到两个分支。通常,递归计算执行几次递归调用,直到终止条件出现(这里,是计算 1 的阶乘),返回某个常量的值,或使用非递归代码计算结果。如果终止条件不正确,代码可能永远循环下去,或最终由于堆栈溢出异常而崩溃。
递归对于函数编程来说是绝对的核心,因此,函数语言已经开发出一些方法和优化机制,避免了即使深度递归调用,也不会出现堆栈溢出的问题。这些以及更高级的主题将在第十章讨论。