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