Tail recursion

In previous labs, we've seen several examples illustrating the idea of separating the recursive kernel of a procedure from a husk that performs the initial call. Sometimes we've done this in order to avoid redundant precondition tests, or to prevent the user from bypassing the precondition tests. In other cases, we saw that the recursion can be written more naturally if the recursive procedure has an additional argument, not supplied by the original caller.

There is yet another reason for adopting the husk-and-kernel approach, and it has to do with efficiency. An implementation of Scheme is required to perform tail-call elimination -- to implement procedure calls in such a way that, if the last step in procedure A is a call to procedure B (so that A will simply return to its caller whatever value is returned by B), the memory resources supporting the call to A can be freed and recycled as soon as the call to B has been started. To make this possible, the implementer arranges for B to return its value directly to A's caller, bypassing A entirely. In particular, this technique is required to work when A and B are the same procedure, invoking itself recursively (in which case the recursion is called tail recursion), and even if there are a number of recursive calls, each of which will return to its predecessor the value returned by its successor. In the implementation, each of the intermediate calls vanishes as soon as its successor is launched.

However, this clever technique, which speeds up procedure calling and sometimes enables Scheme to use memory very efficiently, is guaranteed to work only if the procedure call is the last step. For instance, tail-call elimination cannot be used in the sum procedure as we defined it in an earlier lab:

(define sum
  (lambda (ls)
    (if (null? ls)
        0
        (+ (car ls) (sum (cdr ls))))))

The recursive call in this case is not a tail call, since, after it returns its value, the first number on the list still has to be added to that value.

However, it is possible to write a tail-recursive version of sum:

(define sum
  (lambda (ls)
    (sum-kernel ls 0)))

(define sum-kernel
  (lambda (ls running-total)
    (if (null? ls)
        running-total
        (sum-kernel (cdr ls) (+ (car ls) running-total)))))

The idea is to provide, in each recursive call, a second argument, giving the sum of all the list elements that have been encountered so far: the running total of the previously encountered elements. When the end of the list is reached, the value of this running total is returned; until then, each recursive call strips one element from the beginning of the list, adds it to the running total, and finally calls itself recursively with the shortened list and the augmented running total. The ``finally'' part is important: sum-kernel is tail-recursive.

Here is a summary of the execution of a call to this version of sum:

    (sum (list 97 85 34 73 10))
--> (sum-kernel (list 97 85 34 73 10) 0)
--> (sum-kernel (list 85 34 73 10) 97)
--> (sum-kernel (list 34 73 10) 182)
--> (sum-kernel (list 73 10) 216)
--> (sum-kernel (list 10) 289)
--> (sum-kernel null 299)
--> 299

Note that the additions are performed on the way into the successive calls to sum-kernel, so that when the base case is reached no further calculation is needed -- the value of the second argument in that last call to sum-kernel is returned without further modification as the value of the original call to sum.

As another example, let's consider the following procedure.

Define a procedure index that has two arguments, an item a and a list of items ls, and returns the index of a in ls, that is, the zero-based location of a in ls. If the item is not in the list, the procedure returns -1. Test your procedure on:

  (index 3 '(1 2 3 4 5 6)) ===> 2
  (index 'so '(do re mi fa so la ti do)) ===> 4
  (index 'a '(b c d e)) ===> -1
  (index 'cat '()) ===> -1

The idea is to work down through the elements of the list, keeping track of how many of them we pass. If we reach the end of the list, we return -1; if we encounter the item that we are looking for, we return the number of bypassed items; otherwise, we continue down the list, adding 1 to the number of bypassed items:

(define index
  (lambda (a ls)
    (index-kernel a ls 0)))

(define index-kernel
  (lambda (sought ls bypassed)
    (cond ((null? ls) -1)
          ((equal? (car ls) sought) bypassed)
          (else (index-kernel sought (cdr ls) (+ bypassed 1))))))

There are some cases in which tail recursion is dramatically faster than traditional recursion. Consider the Fibonacci sequence: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ... In the Fibonacci sequence, each number is the sum of the previous two, except for the first two which are 0 and 1. Using this definition we can write a procedure Fibonacci which takes in a nonnegative integer n and returns the nth Fibonacci number, where 0 is the Oth Fibonacci number.

;;; Fibonacci: computes the nth Fibonacci number
;;;
;;; Given: an integer N
;;;
;;; Result: an integer FIB
;;;
;;; Preconditions: N must be nonnegative
;;;
;;; Postconditions: FIB is the Nth Fibonacci number

(define Fibonacci 
  (lambda (n)
    (if (< n 2)
        n
        (+ (Fibonacci (- n 1)) (Fibonacci (- n 2))))))

Unfortunately this version of Fibonacci is quite slow for n much larger than 30. The reason lies in the last line. Notice that one call to (Fibonacci 100) results in one call to (Fibonacci 99), two calls to (Fibonacci 98), ... and 354224848179261915075 calls to (Fiboacci 1). A quick "back of the envelope" calculation suggests that this would take over a hundred centuries!

Fortunately by keeping track of the most recent two Fibonacci numbers and computing from the bottom up, we can write a fast tail-recursive procedure.

(define Fibonacci
  (lambda (n)
    (Fibonacci-kernel 0 1 n)))

(define Fibonacci-kernel
  (lambda (current next remaining)
    (if (= 0 remaining)
        current
        (Fibonacci-kernel next (+ current next) (- remaining 1)))))

This document is available on the World Wide Web as

http://www.cs.grinnell.edu/~gum/courses/151/readings/tail-recursion.xhtml

created March 2, 1997
last revised February 17, 2002

John David Stone (stone@cs.grinnell.edu) and Ben Gum (gum@cs.grinnell.edu)