What is tail recursion?

前端 未结 28 3652
一个人的身影
一个人的身影 2020-11-21 05:03

Whilst starting to learn lisp, I\'ve come across the term tail-recursive. What does it mean exactly?

相关标签:
28条回答
  • 2020-11-21 05:26

    Instead of explaining it with words, here's an example. This is a Scheme version of the factorial function:

    (define (factorial x)
      (if (= x 0) 1
          (* x (factorial (- x 1)))))
    

    Here is a version of factorial that is tail-recursive:

    (define factorial
      (letrec ((fact (lambda (x accum)
                       (if (= x 0) accum
                           (fact (- x 1) (* accum x))))))
        (lambda (x)
          (fact x 1))))
    

    You will notice in the first version that the recursive call to fact is fed into the multiplication expression, and therefore the state has to be saved on the stack when making the recursive call. In the tail-recursive version there is no other S-expression waiting for the value of the recursive call, and since there is no further work to do, the state doesn't have to be saved on the stack. As a rule, Scheme tail-recursive functions use constant stack space.

    0 讨论(0)
  • 2020-11-21 05:27

    This excerpt from the book Programming in Lua shows how to make a proper tail recursion (in Lua, but should apply to Lisp too) and why it's better.

    A tail call [tail recursion] is a kind of goto dressed as a call. A tail call happens when a function calls another as its last action, so it has nothing else to do. For instance, in the following code, the call to g is a tail call:

    function f (x)
      return g(x)
    end
    

    After f calls g, it has nothing else to do. In such situations, the program does not need to return to the calling function when the called function ends. Therefore, after the tail call, the program does not need to keep any information about the calling function in the stack. ...

    Because a proper tail call uses no stack space, there is no limit on the number of "nested" tail calls that a program can make. For instance, we can call the following function with any number as argument; it will never overflow the stack:

    function foo (n)
      if n > 0 then return foo(n - 1) end
    end
    

    ... As I said earlier, a tail call is a kind of goto. As such, a quite useful application of proper tail calls in Lua is for programming state machines. Such applications can represent each state by a function; to change state is to go to (or to call) a specific function. As an example, let us consider a simple maze game. The maze has several rooms, each with up to four doors: north, south, east, and west. At each step, the user enters a movement direction. If there is a door in that direction, the user goes to the corresponding room; otherwise, the program prints a warning. The goal is to go from an initial room to a final room.

    This game is a typical state machine, where the current room is the state. We can implement such maze with one function for each room. We use tail calls to move from one room to another. A small maze with four rooms could look like this:

    function room1 ()
      local move = io.read()
      if move == "south" then return room3()
      elseif move == "east" then return room2()
      else print("invalid move")
           return room1()   -- stay in the same room
      end
    end
    
    function room2 ()
      local move = io.read()
      if move == "south" then return room4()
      elseif move == "west" then return room1()
      else print("invalid move")
           return room2()
      end
    end
    
    function room3 ()
      local move = io.read()
      if move == "north" then return room1()
      elseif move == "east" then return room4()
      else print("invalid move")
           return room3()
      end
    end
    
    function room4 ()
      print("congratulations!")
    end
    

    So you see, when you make a recursive call like:

    function x(n)
      if n==0 then return 0
      n= n-2
      return x(n) + 1
    end
    

    This is not tail recursive because you still have things to do (add 1) in that function after the recursive call is made. If you input a very high number it will probably cause a stack overflow.

    0 讨论(0)
  • 2020-11-21 05:29

    Here is a Common Lisp example that does factorials using tail-recursion. Due to the stack-less nature, one could perform insanely large factorial computations ...

    (defun ! (n &optional (product 1))
        (if (zerop n) product
            (! (1- n) (* product n))))
    

    And then for fun you could try (format nil "~R" (! 25))

    0 讨论(0)
  • 2020-11-21 05:29

    A tail recursive function is a recursive function where the last operation it does before returning is make the recursive function call. That is, the return value of the recursive function call is immediately returned. For example, your code would look like this:

    def recursiveFunction(some_params):
        # some code here
        return recursiveFunction(some_args)
        # no code after the return statement
    

    Compilers and interpreters that implement tail call optimization or tail call elimination can optimize recursive code to prevent stack overflows. If your compiler or interpreter doesn't implement tail call optimization (such as the CPython interpreter) there is no additional benefit to writing your code this way.

    For example, this is a standard recursive factorial function in Python:

    def factorial(number):
        if number == 1:
            # BASE CASE
            return 1
        else:
            # RECURSIVE CASE
            # Note that `number *` happens *after* the recursive call.
            # This means that this is *not* tail call recursion.
            return number * factorial(number - 1)
    

    And this is a tail call recursive version of the factorial function:

    def factorial(number, accumulator=1):
        if number == 0:
            # BASE CASE
            return accumulator
        else:
            # RECURSIVE CASE
            # There's no code after the recursive call.
            # This is tail call recursion:
            return factorial(number - 1, number * accumulator)
    print(factorial(5))
    

    (Note that even though this is Python code, the CPython interpreter doesn't do tail call optimization, so arranging your code like this confers no runtime benefit.)

    You may have to make your code a bit more unreadable to make use of tail call optimization, as shown in the factorial example. (For example, the base case is now a bit unintuitive, and the accumulator parameter is effectively used as a sort of global variable.)

    But the benefit of tail call optimization is that it prevents stack overflow errors. (I'll note that you can get this same benefit by using an iterative algorithm instead of a recursive one.)

    Stack overflows are caused when the call stack has had too many frame objects pushed onto. A frame object is pushed onto the call stack when a function is called, and popped off the call stack when the function returns. Frame objects contain info such as local variables and what line of code to return to when the function returns.

    If your recursive function makes too many recursive calls without returning, the call stack can exceed its frame object limit. (The number varies by platform; in Python it is 1000 frame objects by default.) This causes a stack overflow error. (Hey, that's where the name of this website comes from!)

    However, if the last thing your recursive function does is make the recursive call and return its return value, then there's no reason it needs to keep the current frame object needs to stay on the call stack. After all, if there's no code after the recursive function call, there's no reason to hang on to the current frame object's local variables. So we can get rid of the current frame object immediately rather than keep it on the call stack. The end result of this is that your call stack doesn't grow in size, and thus cannot stack overflow.

    A compiler or interpreter must have tail call optimization as a feature for it to be able to recognize when tail call optimization can be applied. Even then, you may have rearrange the code in your recursive function to make use of tail call optimization, and it's up to you if this potential decrease in readability is worth the optimization.

    0 讨论(0)
提交回复
热议问题