Session 11
Recursive Functions and Loops

Download this Racket file to use as you work through Session 11.

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>

Now, for the exercise:

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-exp.

For a twist:

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 the same 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 f nlst)
  (if (null? nlst)
      '()
      (cons (map-numexp f (first nlst))
            (map-nlist  f (rest nlst)))))

(define (map-numexp 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 is the function that 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:

  • interface procedures — When our structurally-recursive function requires an argument that the function specification does not provide, we turn it into a helper function. The specified function passes its original arguments to the helper, along with an initial value for the new argument.
  • mutual recursion — When our inductive data definition includes two data structures that are defined in terms of one another, we write two functions that are defined in terms of one another.

Program Derivation

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 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.

Relationships Among the Recursive Programming Patterns

With program derivation, we have now seen four recursive programming patterns. We start with structural recursion and apply the other patterns when we encounter specific issues.

a directed acyclic graph showing the four recursive programming patterns, connected by edges labeled with the forces that lead us to apply them
The structurally recursive programming patterns

These patterns will guide us as we write functions to process any inductively-defined data types.

A Program Derivation Exercise

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

(define (map-numexp 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 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 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, that trade-off is worth it. 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

Odd, indeed.

When we call factorial-aps, this is how the computation starts:
(factorial-aps 6 1)     ; the function call
(factorial-aps 5 6)     ; the first recursive call
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 factorial-aps is written this way.

But notice: It passes a partial answer along with every recursive call and returns that 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. In Python, the n = 6 example is:

n = 6
answer = 1
while (n > 0):
    answer = n * answer
    n = n - 1

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.

The first call to the function is (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:

(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/
                                    |        }))

Observing This Behavior with trace

We can see this behavior with the help of a Racket module named racket/trace. Add these lines to the source file containing factorial-aps and factorial:

(require racket/trace)
(trace factorial)
(trace factorial-aps)

Now call (factorial 6). The function shows us its behavior on the run-time stack, adding a new stack frame with each recursive call.

Then call (factorial-aps 6 1). This function shows its behavior, too. There are no new stack frames, only changes to the parameters on the original stack frame.

This is how Racket can be so efficient for many recursive functions. It does not generate new stack frames!

Proper Handling of Tail Calls

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

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 often 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 an active 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, check out this optional reading 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 a solution.

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

Wrap Up