递归函数:rec 关键字
rec
关键字与 let
关键字一起用于定义递归函数。
语法
// Recursive function:
let rec function-name parameter-list =
function-body
// Mutually recursive functions:
let rec function1-name parameter-list =
function1-body
and function2-name parameter-list =
function2-body
...
备注
在 F# 语言中,递归函数(调用自身的函数)使用 rec
关键字标识。 rec
关键字使 let
绑定的名称在其主体中可用。
下面的示例演示一个递归函数,该函数使用数学定义计算第 n 个斐波纳契数。
let rec fib n =
match n with
| 0 | 1 -> n
| n -> fib (n-1) + fib (n-2)
注意
实际上,像前面的示例这样的代码并不理想,因为它不必要地重新计算了已经计算过的值。 这是因为它不是尾递归函数,本文对此进行了进一步说明。
方法在定义方法的类型内是隐式递归的,这意味着无需添加 rec
关键字。 例如:
type MyClass() =
member this.Fib(n) =
match n with
| 0 | 1 -> n
| n -> this.Fib(n-1) + this.Fib(n-2)
但是,类中的 let
绑定不是隐式递归的。 所有 let
绑定函数都需要 rec
关键字。
尾递归
对于某些递归函数,需要将更“纯粹”的定义重构为尾递归。 这可以防止不必要的重新计算。 例如,可以重写以前的斐波纳契数生成器,如下所示:
let fib n =
let rec loop acc1 acc2 n =
match n with
| 0 -> acc1
| 1 -> acc2
| _ ->
loop acc2 (acc1 + acc2) (n - 1)
loop 0 1 n
生成斐波那契数是“朴素”算法的一个很好的例子,它在数学上是纯粹的,但在实践中效率低下。 虽然这是一个更复杂的实现,但在 F# 中,有几个方面可以提高效率,同时仍以递归方式进行定义:
- 一个名为
loop
的递归内部函数,它是一种惯用的 F# 模式。 - 两个累加器参数,用于将累加值传递给递归调用。
- 检查
n
的值以返回特定的累加器。
如果此示例是使用循环以迭代方式编写的,则代码看起来很相似,两个不同的值累加数字,直到满足特定条件。
这属于尾递归的原因是递归调用不需要在调用堆栈上保存任何值。 要计算的所有中间值通过内部函数的输入累加。 这还允许 F# 编译器优化代码,使其与编写 while
循环之类的东西一样快。
如前面的示例所示,编写 F# 代码,以递归方式处理具有内部和外部函数的内容是很常见的。 内部函数使用尾递归,而外部函数为调用者提供更好的接口。
从 F# 8.0 开始,你可以使用 TailCall
属性向编译器显式声明定义尾递归函数的意图。 如果函数进行非尾递归调用,编译器会警告你。 可以在方法和模块级函数上使用该属性。
例如,在第一个 fib
定义上使用它:
[<TailCall>]
let rec fib n =
match n with
| 0 | 1 -> n
| n -> fib (n-1) + fib (n-2)
将触发编译器关于两个非尾递归调用的警告。
相互递归函数
有时,函数是相互递归的,这意味着调用形成一个循环,其中一个函数调用另一个函数,而后者又调用第一个函数,其间有任意数量的调用。 必须在一个 let
绑定中一起定义这些函数,使用 and
关键字将它们链接在一起。
下面的示例演示两个相互递归函数。
let rec Even x = if x = 0 then true else Odd(x - 1)
and Odd x = if x = 0 then false else Even(x - 1)
递归值
还可以将 let
绑定的值定义为递归值。 这有时是为了进行日志记录。 使用 F# 5 和 nameof
函数,可以执行以下工作:
let rec nameDoubles = nameof nameDoubles + nameof nameDoubles