簡體   English   中英

F# 尾遞歸函數示例

[英]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

由於對loopcont的調用是最后調用且無需額外工作的函數,因此它們是尾遞歸的。

這是有效的,因為延續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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM