共用方式為


遞迴函式: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 Fibonacci 數位的遞歸函式。

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 關鍵詞。

尾遞歸

對於某些遞歸函式,必須將更「純」的定義重構為 尾遞歸。 這可防止不必要的重新計算。 例如,先前的 Fibonacci 數位產生器可以像這樣重寫:

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

產生 Fibonacci 數位是「天真」演算法的絕佳範例,在數學上純粹但實際上效率不佳。 雖然這是更複雜的實作,但數個層面讓 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)

會觸發兩個非尾遞歸呼叫的編譯程式警告。

相互遞歸函式

有時候函式 是相互遞迴的,這表示呼叫會形成圓形,其中一個函式會呼叫另一個函式,接著呼叫第一個函式,並在兩者之間有任意數目的呼叫。 您必須在一個系結中定義這類函式,並使用 and 關鍵詞將它們連結在一let起。

下列範例顯示兩個相互遞歸函式。

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

另請參閱