Session 11
Recursive Functions and Loops

A Warm-Up Exercise

As we have seen, map is a higher-order function that's awfully handy for solving problems with lists. However, it does not work the way we want when used with nested lists such as the s-lists we learned about last time. So let's make our own special-purpose map!

Let's work with a new kind of nested list, the n-list. N-lists are just like s-lists, but with numbers:

    <n-list> ::= ()
               | (<number-exp> . <n-list>)

<number-exp> ::= <number>
               | <n-list>

Implement (map-nlist f nlist), where f is a function that takes a single number argument and nlist is an n-list.

map-nlist returns a list with the same structure as nlst, but where each number n has been replaced with (f n). For example:

> (map-nlist even? '(1 4 9 16 25 36 49 64))
'(#f #t #f #t #f #t #f #t)

> (map-nlist add1 '(1 (4 (9 (16 25)) 36 49) 64))
'(2 (5 (10 (17 26)) 37 50) 65)

map-nlist should be mutually recursive with map-numexp, which operates on number-exps.

For a twist: if you are seated in the front two rows, write map-nlist; if you are seated in the back two rows, write map-numexp. If you finish before I call time, write the other function, too.

a photo of a white cat from the top; the back of the white cat page is a black patch that looks like a cat
Hello, recursive kitty.

Using Mutual Recursion to Implement map-nlist

The definition on n-list is mutually inductive, so let's use mutual recursion. The code will look quite a bit like our mutually-recursive subst function from last time.

We build the function from scratch...

The result is something like this:

(define map-nlist
  (lambda (f nlst)
    (if (null? nlst)
        '()
        (cons (map-numexp f (first nlst))
              (map-nlist  f (rest nlst))))))

(define map-numexp
  (lambda (f numexp)
    (if (number? numexp)
        (f numexp)
        (map-nlist f numexp))))

This is quite nice. map-nlist says exactly what it does: combine the result of mapping f over the numbers in the car with the result of mapping f over the numbers in the cdr. There is no extra detail. map-numexp applies the function f, because it is the only code that ever sees a number! If it sees an n-list, it lets map-nlist do the job.

With small steps and practice, this sort of thinking can become as natural to you as writing for loops and defining functions in some other style.

Recap: Writing Recursive Programs

In the last two sessions, we have studied how to write recursive programs based on inductively-defined data. Our basic technique is structural recursion, which asks us to mimic the structure of the data we are processing in our function. We then learned two techniques for writing recursive programs when our basic technique needs a little help:

In your reading for this time, you also encountered the idea of program derivation. Mutual recursion creates two functions that call each other. Sometimes, the cost of the extra function calls is high enough that we would like to improve our code, while remaining as faithful as possible to the inductive data definition. Program derivation helps us eliminate the extra function calls without making a mess of our code.

Program derivation is a fancy name for an idea we already understand at some level. In Racket, expressions are evaluated by repeatedly substituting values. Suppose we have a simple function:

(define 2n-plus-1
  (lambda (n)
    (add1 (* 2 n))))

Whenever we use the name 2n-plus-1, Racket evaluates it and gets the lambda expression that it names. To evaluate a call to the function, Racket does what you expect: it evaluates the arguments and substitutes them for the formal parameters in the function body. Thus we go from the application of a named function, such as:

(2n-plus-1 15)

to the application of a lambda expression:

((lambda (n)
    (add1 (* 2 n)))
 15)

If we stopped here, we would still be making the function call. But we can apply the next step in the substitution model to convert the function call into this expression:

(add1 (* 2 15))

Our compilers can often do this for us. As noted in your reading on program derivation, a Java compiler will generally inline calls to simple access methods, and C++ provides an inline keyword that lets the programmer tell the compiler to translate away calls to a specific function whenever possible.

As a programming technique, program derivation enables us to refactor code in a way that results in a more efficient program without sacrificing too many of the benefits that come with writing a second function.

Work through program derivation example from the reading: count-occurrences.
It produces the same solution, but a different function.

A Program Derivation Exercise

Use program derivation to convert map-nlist into a single function.
(define map-nlist
  (lambda (f nlst)
    (if (null? nlst)
        '()
        (cons (map-numexp f (first nlst))
              (map-nlist f (rest nlst))))))

(define map-numexp
  (lambda (f numexp)
    (if (number? numexp)
        (f numexp)
        (map-nlist f numexp))))

map-nlist, Refactored

First, we substitute the lambda for the name:

(define map-nlist
  (lambda (f nlst)
    (if (null? nlst)
        '()
        (cons ((lambda (f numexp)
                  (if (number? numexp)
                      (f numexp)
                      (map-nlist f numexp)))
                f (first nlst))
              (map-nlist f (rest nlst))))))

... and then the partially-evaluated body for the lambda:

(define map-nlist
  (lambda (f nlst)
    (if (null? nlst)
        '()
        (cons (if (number? (first nlst))
                  (f (first nlst))
                  (map-nlist f (first nlst)))
              (map-nlist f (rest nlst))))))

This eliminates the back-and-forth function calls between map-nlist and map-numexp. The primary cost is an apparent loss of readability: the resulting function is more complex than the original two. Sometimes, the trade-off is worth it, and as you become a more confident Racket programmer you will find the one-function solution a bit easier to grok immediately.

We will use program derivation only when we really need it, or when the resulting code is still small and easy to understand.

Quick Exercise: Expensive Recursion

When we first learn about recursion, we often use it to implement functions such as factorial and fibonacci. Here is what the typical factorial function looks like in Racket:

(define factorial
  (lambda (n)
    (if (zero? n) 
        1
        (* n (factorial (sub1 n))))))

Back in Session 2, though, I showed you an odd-looking implementation:

(define factorial-aps
  (lambda (n answer)
    (if (zero? n)
        answer
        (factorial-aps (sub1 n) (* n answer)))))

> (factorial-aps 6 1)
720

The first recursive call to factorial-aps is:

(factorial-aps 5 6)

Your task: Write down the rest of the recursive calls.

a picture of a cat curled up, with its tail in its front paws, captioned 'tail recursion' in capital letters
Hello, tail recursive kitty.

Tail Recursion

You may wonder why the function is written this way. It passes a partial answer along with every recursive call and returns the partial answer when it finishes.

In a very real sense, this function is iterative. It counts down from n to 0, accumulating partial solutions along the way. Consider the sequence of calls made for n = 6:

(factorial-aps  6    1)
(factorial-aps  5    6)
(factorial-aps  4   30)
(factorial-aps  3  120)
(factorial-aps  2  360)
(factorial-aps  1  720)
(factorial-aps  0  720)

This function is also imperative. Its only purpose on each recursive call is to assign new values to n and the accumulator variable. In functional style, though, we pass the new values for the "variable" as arguments on a recursive call.

That sounds a lot like the for loop we would write in an imperative language. On each pass through the loop, we update our running sum and decrement our counter.

At run time, factorial-aps can be just like a loop! Consider the state of the calling function at the moment it makes its recursive call. The value to be returned by the calling function is the same value that will be returned by the called function! The caller does not need to remember any pending operations or even the values of its formal parameters. There is no work left to be done.

Consider our first call to the function, (factorial-aps 6 1). The formal parameters are n = 6 and answer = 1. The value of this call will be the value returned by its body, which is:

(if (zero? n)
    answer
    (factorial-aps (- n 1) (* n answer)))

n is not zero, so the value of the if expression will be the value of the else clause. The else clause is

(factorial-aps (- n 1) (* n answer))

which means that the recursive function will call be:

(factorial-aps 5 6)

Whatever (factorial-aps 5 6) returns will be returned as the value of the else clause, which becomes the value of the if expression, which becomes the value of (factorial-aps 6 1).

That's what we mean above: The value to be returned by the calling function — which is (factorial-aps 6 1) — will be the same value that is returned by the called function — which is (factorial-aps 5 6). When (factorial-aps 5 6) returns a value, (factorial-aps 6 1) passes it on as its own answer.

In programming languages, the last expression to evaluate in order to know the value of an expression is called the tail call. We call it that because it is the "tail" of the computation.

In the case of factorial-aps, the tail call is a call to factorial-aps itself. In programming languages, we call this function tail recursive.

When a function is tail-recursive, the interpreter or compiler can take advantage of the fact that the value returned by the calling function is the same as the value returned by the called function to generate more efficient code. How?

It can implement the recursive call "in place", reusing the same stack frame. First, it stores the values passed in the tail call into the same slots that hold the formal parameters of the calling function. Second, it replaces the function call with a goto statement, transferring control back to the top of the calling function.

(lambda (n ans)                     |  (lambda (n ans)
  (if (zero? n)                     |    (if (zero? n)
      ans                           |        ans
                                    |        {
      (factorial-aps (sub1 n)       |          n := (sub1 n)
                      (* n ans))))  |          ans := (* n ans)
                                    |          goto /if/
                                    |        }

Illustrate stack frames. Use plain factorial and factorial-aps.

By definition, a Racket compiler does this. The Scheme language definition specifies that every Scheme interpreter must optimize tail calls into equivalent gotos. Racket, a descendant of Scheme, is faithful to this handling of tail calls.

Illustrate in code. Use racket/trace to trace both functions.

Not all languages do this. The presence of side effects and other complex forms in a language can cause exceptions to the handy return behavior we see in tail recursion. Compilers for such languages usually opt to to be conservative.

For example, Java does not handle tail calls properly, and making it do so would complicate the virtual machine a bit. So the handlers of Java have not made this a requirement for compilers. Likewise for Python. Still, many programmers think it might be worth the effort. Some Java compilers do optimize tail recursion under certain circumstances, as does the GNU C/C++ compiler gcc. Tail recursion remains a hot topic in programming languages.

With their lack of side effects, functional programming languages are a natural place to properly handle tail calls. In addition to Racket, languages such as Haskell make good use of tail call elimination. That leads to some interesting new design patterns as well.

In functional programming, we use recursion for all sorts of repetitive behavior. We often use tail recursion because, as we have seen, the run-time behavior of non-tail recursive functions can be so bad. In other cases, we use tail recursion because structuring our code in this way enables other design patterns that we desire.

The second argument to our factorial function above is sometimes called an accumulator variable. How do we create one when writing a recursive function? If you'd like to learn more about this programming pattern, read this optional section on accumulator variables.

Using an Interface Procedure to Implement positions-of

You had an opportunity to practice using interface procedures on Homework 4. It has an interesting property we can now see...

positions-of required us to create a helper function that kept track of the position number of each item in the list.

Build it if there is time, else look at solution.

Notice: positions-of-at is naturally tail recursive!

Wrap Up