[英]F# Tail Recursive Function Example
我是 F# 的新手,正在閱讀有關尾遞歸函數的內容,並希望有人能給我兩種不同的函數 foo 實現——一種是尾遞歸的,另一種不是尾遞歸的,這樣我就可以更好地理解原理。
從一個簡單的任務開始,比如將列表中的項目從“a”映射到“b”。 我們想寫一個具有簽名的函數
val map: ('a -> 'b) -> 'a list -> 'b list
在哪里
map (fun x -> x * 2) [1;2;3;4;5] == [2;4;6;8;10]
從非尾遞歸版本開始:
let rec map f = function
| [] -> []
| x::xs -> f x::map f xs
這不是尾遞歸,因為函數在進行遞歸調用后仍有工作要做。 ::
是List.Cons(fx, map f xs)
語法糖。
如果我將最后一行重寫為| x::xs -> let temp = map f xs; fx::temp
該函數的非遞歸性質可能會更明顯一些| x::xs -> let temp = map f xs; fx::temp
| x::xs -> let temp = map f xs; fx::temp
| x::xs -> let temp = map f xs; fx::temp
- 顯然它在遞歸調用后工作。
使用累加器變量使其尾遞歸:
let map f l =
let rec loop acc = function
| [] -> List.rev acc
| x::xs -> loop (f x::acc) xs
loop [] l
這是我們在變量acc
建立一個新列表。 由於列表是反向構建的,我們需要在將輸出列表返回給用戶之前反轉輸出列表。
如果你有點頭腦扭曲,你可以使用延續傳遞來更簡潔地編寫代碼:
let map f l =
let rec loop cont = function
| [] -> cont []
| x::xs -> loop ( fun acc -> cont (f x::acc) ) xs
loop id l
由於對loop
和cont
的調用是最后調用且無需額外工作的函數,因此它們是尾遞歸的。
這是有效的,因為延續cont
被一個新的延續捕獲,而新的延續又被另一個延續捕獲,從而產生一種樹狀數據結構,如下所示:
(fun acc -> (f 1)::acc)
((fun acc -> (f 2)::acc)
((fun acc -> (f 3)::acc)
((fun acc -> (f 4)::acc)
((fun acc -> (f 5)::acc)
(id [])))))
它按順序構建列表,而無需您將其反轉。
不管它的價值是什么,開始以非尾遞歸方式編寫函數,它們更易於閱讀和使用。
如果您有一個很大的列表要查看,請使用累加器變量。
如果您找不到以方便的方式使用累加器的方法,並且您沒有任何其他選擇,請使用延續。 我個人認為難以閱讀的非平凡的、大量使用延續的。
嘗試比其他示例更簡短的解釋:
let rec foo n =
match n with
| 0 -> 0
| _ -> 2 + foo (n-1)
let rec bar acc n =
match n with
| 0 -> acc
| _ -> bar (acc+2) (n-1)
這里, foo
不是尾遞歸的,因為 foo 必須遞歸調用foo
才能計算2+foo(n-1)
並返回它。
但是, bar
是尾遞歸的,因為bar
不必使用遞歸調用的返回值來返回值。 它可以讓遞歸調用的bar
立即返回其值(無需通過調用堆棧一直向上返回)。 編譯器看到這一點並通過將遞歸重寫為循環來優化它。
將bar
的最后一行更改為類似| _ -> 2 + (bar (acc+2) (n-1))
| _ -> 2 + (bar (acc+2) (n-1))
將再次破壞函數是尾遞歸的,因為2 +
導致一個動作,該遞歸調用完成之后進行的需求。
這是一個更明顯的例子,將它與您通常對階乘所做的進行比較。
let factorial n =
let rec fact n acc =
match n with
| 0 -> acc
| _ -> fact (n-1) (acc*n)
fact n 1
這個有點復雜,但這個想法是你有一個累加器來保持運行記錄,而不是修改返回值。
此外,這種包裝風格通常是一個好主意,這樣您的調用者就不必擔心為累加器設置種子(請注意,事實是函數的局部變量)
我也在學習 F#。 以下是計算斐波那契數的非尾遞歸和尾遞歸函數。
非尾遞歸版本
let rec fib = function
| n when n < 2 -> 1
| n -> fib(n-1) + fib(n-2);;
尾遞歸版本
let fib n =
let rec tfib n1 n2 = function
| 0 -> n1
| n -> tfib n2 (n2 + n1) (n - 1)
tfib 0 1 n;;
注意:由於斐波那契數可能增長得非常快,您可以將最后一行tfib 0 1 n
替換為
tfib 0I 1I n
利用 F# 中的 Numerics.BigInteger 結構
另外,在測試時,不要忘記在調試模式下編譯時默認關閉間接尾遞歸(tailcall)。 這會導致尾調用遞歸在調試模式下溢出堆棧,但在發布模式下不會。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.