简体   繁体   中英

F# Tail Recursive Function Example

我是 F# 的新手,正在阅读有关尾递归函数的内容,并希望有人能给我两种不同的函数 foo 实现——一种是尾递归的,另一种不是尾递归的,这样我就可以更好地理解原理。

Start with a simple task, like mapping items from 'a to 'b in a list. We want to write a function which has the signature

val map: ('a -> 'b) -> 'a list -> 'b list

Where

map (fun x -> x * 2) [1;2;3;4;5] == [2;4;6;8;10]

Start with non-tail recursive version:

let rec map f = function
    | [] -> []
    | x::xs -> f x::map f xs

This isn't tail recursive because function still has work to do after making the recursive call. :: is syntactic sugar for List.Cons(fx, map f xs) .

The function's non-recursive nature might be a little more obvious if I re-wrote the last line as | 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 -- obviously its doing work after the recursive call.

Use an accumulator variable to make it tail recursive:

let map f l =
    let rec loop acc = function
        | [] -> List.rev acc
        | x::xs -> loop (f x::acc) xs
    loop [] l

Here's we're building up a new list in a variable acc . Since the list gets built up in reverse, we need to reverse the output list before giving it back to the user.

If you're in for a little mind warp, you can use continuation passing to write the code more succinctly:

let map f l =
    let rec loop cont = function
        | [] -> cont []
        | x::xs -> loop ( fun acc -> cont (f x::acc) ) xs
    loop id l

Since the call to loop and cont are the last functions called with no additional work, they're tail-recursive.

This works because the continuation cont is captured by a new continuation, which in turn is captured by another, resulting in a sort of tree-like data structure as follows:

(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 [])))))

which builds up a list in-order without requiring you to reverse it.


For what its worth, start writing functions in non-tail recursive way, they're easier to read and work with.

If you have a big list to go through, use an accumulator variable.

If you can't find a way to use an accumulator in a convenient way and you don't have any other options at your disposal, use continuations. I personally consider non-trivial, heavy use of continuations hard to read.

An attempt at a shorter explanation than in the other examples:

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)

Here, foo is not tail-recursive, because foo has to call foo recursively in order to evaluate 2+foo(n-1) and return it.

However, bar ís tail-recursive, because bar doesn't have to use the return value of the recursive call in order to return a value. It can just let the recursively called bar return its value immediately (without returning all the way up though the calling stack). The compiler sees this and optimized this by rewriting the recursion into a loop.

Changing the last line in bar into something like | _ -> 2 + (bar (acc+2) (n-1)) | _ -> 2 + (bar (acc+2) (n-1)) would again destroy the function being tail-recursive, since 2 + leads to an action that needs to be done after the recursive call is finished.

Here is a more obvious example, compare it to what you would normally do for a factorial.

let factorial n =
    let rec fact n acc =
        match n with
        | 0 -> acc
        | _ -> fact (n-1) (acc*n)
    fact n 1

This one is a bit complex, but the idea is that you have an accumulator that keeps a running tally, rather than modifying the return value.

Additionally, this style of wrapping is usually a good idea, that way your caller doesn't need to worry about seeding the accumulator (note that fact is local to the function)

I'm learning F# too. The following are non-tail recursive and tail recursive function to calculate the fibonacci numbers.

Non-tail recursive version

let rec fib = function
    | n when n < 2 -> 1
    | n -> fib(n-1) + fib(n-2);;

Tail recursive version

let fib n =
    let rec tfib n1 n2 = function
    | 0 -> n1
    | n -> tfib n2 (n2 + n1) (n - 1)
    tfib 0 1 n;;  

Note: since the fibanacci number could grow really fast you could replace last line tfib 0 1 n to
tfib 0I 1I n to take advantage of Numerics.BigInteger Structure in F#

Also, when testing, don't forget that indirect tail recursion (tailcall) is turned off by default when compiling in Debug mode. This can cause tailcall recursion to overflow the stack in Debug mode but not in Release mode.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM