18B. Tail Recursion

What is tail recursion?

There is a special kind of recursion where the cost of using recursion is much smaller than in the general case. Tail recursion is a situation where a recursive call is the last thing a function does before returning, and the function either returns the result of the recursive call or (when the return type is void) returns no result.

Looking at the definition of nextPrime on the preceding page, we see:

   return nextPrime(n+1);  
That is tail recursion. The function does nothing with the result of the recursive call except to return it. The recursive call in the definition of member,
   return member(x, A, n-1);
is also tail recursion, as is the recursive call
   return allPositive(A, n-1);
in the definition of allPositive. Of course, there are non-tail recursive calls. The recursive call in the definition of sum on a previous page,
   return sum(A, n-1) + A[n-1];
is not tail-recursive. After getting the result sum(A, n−1), sum adds A[n−1] to that result.

What is the significance of tail recursion?

A compiler such as g++ can recognize tail recursion and replace it by a loop, making it more efficient in both time and memory. (But a compiler only does that when it is asked to optimize, which the g++ compiler does if you use command-line option -O.)

For illustration, let's convert the tail-recursive definition of allPositive to a loop in a way that is similar to what a compiler does. The starting point is the tail-recursive definition of allPositive, as follows.

  bool allPositive(int A[], int n)
  {
    if(n == 0)
    {
      return true;
    }
    else if(A[n-1] <= 0)
    {
      return false;
    }
    else
    {
      return allPositive(A, n-1);
    }
  }

Because the recursive call is tail recursive, nothing in a given frame of allPositive needs to be remembered when the recursive call is done. After all, none of that information will be used. The compiler can safely convert the tail-recursive call into a change of the values of parameters, followed by a jump back to the beginning of the function body. That jump is done by wrapping an (infinite) loop around the function body.

  bool allPositive(int A[], int n)
  {
    while(true)
    {
      if(n == 0)
      {
        return true;
      }
      else if(A[n-1] <= 0)
      {
        return false;
      }
      else
      {
        n = n - 1;
	// continue the loop.
      }
    }
  }

You would not be allowed to change the value of n, since it is a const parameter. But the compiler is not bound by that restriction; it can do whatever it wants to do, as long as the translated program does what it is supposed to do.

The nice thing about tail recursion is that you do not need to convert it to a loop; you can let the compiler do that for you. If you find the recursive definition easier to write, you don't pay a performance penalty for using it.

Exercises

  1. What is the advantage of tail-recursion over general recursion? Answer

  2. Is the following definition of g tail-recursive?

      int g(int n)
      {
        if(n == 1) 
        {
           return 2;
        }
        else
        {
           return g(n-1) + 3;
        }
      }
    
    Answer

  3. Write a definition of factorialTimes(n, m), which returns m×n!. Make the definition use tail recursion. Answer