In your work so far, you’ve seen how to repeat actions using one basic
type: the list. You can use map
to apply each element of the list, or
reduce
to merge together neighboring elements, or filter
to extract
certain elements.
But what if you want to do other kinds of repetition? For example, what
if you want to do something for every integer between 1 and 100, of if you
want to do something for every third element of a list? And what if things
like reduce
were not provided by the csc151
library? Since it is not
possible to predict every way that a programmer will want to repeat code,
most languages provide a general mechanism for repeating instructions. In
Scheme, the most general mechanism for repeating instructions is called
recursion, and involves having a procedure use itself as a helper.
That is, a recursive procedure calls itself.
But what does it mean to have a procedure “call itself”? For example, to find the distance between two points, we take the square root of the sum of the squares of the x and y differences.
(define distance
(lambda (x0 y0 x1 y1)
(sqrt (+ (square (- x0 x1))
(square (- y0 y1))))))
Here, we are using both square
and sqrt
as helper procedures and
we say that distance
“calls” square
and sqrt
. (We also call
the addition and subtraction operations helpers.)
Direct recursion is the special case of this construction in which the body of a procedure includes one or more calls to the very same procedure—calls that deal with simpler or smaller arguments.
For instance, let’s define a procedure called sum
that takes one
argument, a list of numbers, and returns the result of adding all of
the elements of the list together:
> (sum (list 91 85 96 82 89))
443
> (sum (list -17 17 12 -4))
8
> (sum (list 9.3))
9.3
> (sum null)
0
You already know one way to compute sum
: You just reduce the list using
addition. But we are going to suppose that the reduce
operation
does not exist or that we want to guarantee that we do the addition in
a particular order.
Because the list to which we apply sum
may have any number of elements,
we can’t just pick out the numbers using list-ref
and add them
up—there’s no way to know in general whether an element even exists at
the position specified by the second argument to list-ref
. One thing
we do know about lists, however, is that every list is either empty
or composed of a first element and a list of the rest of the elements,
which we can obtain with the car
and cdr
procedures.
Moreover, we can use the predicate null?
to distinguish between the two
cases, and conditional evaluation to make sure that only the expression
for the appropriate case is chosen. So the structure of our definition
is going to look something like this:
(define sum
(lambda (numbers)
(if (null? numbers)
; The sum of an empty list
; The sum of a non-empty list
)))
The sum of the empty list is easy—since there’s nothing to add, the total is 0.
And we know that in computing the sum of a non-empty list, we can use
(car numbers)
, which is the first element, and (cdr numbers)
,
which is the rest of the list. So the problem is to find the sum of a
non-empty list, given the first element and the rest of the list. Well,
the rest of the list is one of those “simpler or smaller” arguments
mentioned above. Since Scheme supports direct recursion, we can invoke
the sum
procedure within its own definition to compute the sum of the
elements of the rest of a non-empty list. Add the first element to this
sum, and we’re done! We’d express that portion as follows.
(+ (car numbers) (sum (cdr numbers)))
As we put it together into a complete procedure, we’ll also add some documentation.
;;; Procedure:
;;; sum
;;; Parameters:
;;; numbers, a list of numbers.
;;; Purpose:
;;; Find the sum of the elements of a given list of numbers
;;; Produces:
;;; total, a number.
;;; Preconditions:
;;; All the elements of numbers must be numbers.
;;; Postcondition:
;;; total is the result of adding together all of the elements of numbers.
;;; If all the values in numbers are exact, total is exact.
;;; If any values in numbers are inexact, total is inexact.
(define sum
(lambda (numbers)
(if (null? numbers)
0
(+ (car numbers) (sum (cdr numbers))))))
At first, this may look strange or magical, like a circular definition:
If Scheme has to know the meaning of sum
before it can process the
definition of sum
, how does it ever get started?
The answer is that what Scheme learns from a procedure definition is not so much the meaning of a word as the algorithm, the step-by-step method, for solving a problem. Sometimes, in order to solve a problem, you have to solve another, somewhat simpler problem of the same sort. There’s no difficulty here as long as you can eventually reduce the problem to one that you can solve directly.
Another way to think about it is in terms of the way we normally write instructions. We often say “go back to the beginning and do the steps again”. Given that we’ve named the steps in the algorithm, the recursive call is, in one sense, a way to tell the computer to go back to the beginning.
The strategy of repeatedly solving simpler problems is how Scheme proceeds
when it deals with a call to a recursive procedure – say, (sum (list
38 12 83))
. First, it checks to find out whether the list it is given
is empty. In this case, it isn’t. So we need to determine the result
of adding together the value of (car ls)
, which in this case is 38,
and the sum of the elements of (cdr ls)
, the rest of the given list.
The rest of the list at this point is the value of (list 12 83)
.
How do we compute its sum? We call the sum
procedure again. This list
of two elements isn’t empty either, so again we wind up in the alternate
of the if
-expression. This time we want to add 12, the first element,
to the sum of the rest of the list. By “rest of the list”, this time,
we mean the value of (list 83)
, a one-element list.
To compute the sum of this one-element list, we again invoke the sum
procedure. A one-element list still isn’t empty, so we head once more
into the alternate of the if
-expression, adding the car, 83, to the
sum of the elements of the cdr, null
. The “rest of the list” this time
around is empty, so when we invoke sum
yet one more time, to determine
the sum of this empty list, the test in the x if
-expression succeeds
and the consequent, rather than the alternate, is selected. The sum of
null
is 0.
We now have to work our way back out of all the procedure calls that have
been waiting for arguments to be computed. The sum of the one-element
list, you’ll recall, is 83 plus the sum of null
, that is, 83 + 0,
or just 83. The sum of the two-element list is 12 plus the sum of the
(list 82)
, that is, 12 + 83, or 95. Finally, the sum of the
original three-element list is 38 plus the sum of (list 12 83)
.
Here is a summary of some of the key steps in the evaluation process. (We’ve left out the evaluation of the conditionals, since they are fairly straightforward.)
(sum (list 38 12 83))
=> (+ 38 (sum (12 83)))
=> (+ 38 (+ 12 (sum (list 83))))
=> (+ 38 (+ 12 (+ 83 (sum null))))
=> (+ 38 (+ 12 (+ 83 0)))
=> (+ 38 (+ 12 83))
=> (+ 38 95)
=> 133
Talk about delayed gratification! That’s a while to wait before we can do the first addition.
The process is exactly the same, by the way, regardless of whether we
construct the three-element list using list
, as in the example above,
or as (cons 38 (cons 12 (cons 83 null)))
or even as '(38 12 83)
. Since
we get the same list in each case, sum
takes it apart in exactly the
same way no matter what mechanism was used to build it.
The method of recursion works in this case because each time we invoke
the sum
procedure, we give it a list that is a little shorter and so
a little easier to deal with, and eventually we reach the base case
of the recursion – the empty list – for which the answer can be
computed immediately.
If, instead, the problem became harder or more complicated on each recursive invocation, or if it were impossible ever to reach the base case, we’d have a runaway recursion – a programming error that shows up in MediaScript not as a diagnostic message printed in red, but as an apparently endless wait for a result. The good news is that you can hit the Stop button to stop the computation. While MediaScript should behave normally after you click Stop, we do recommend that you save your code after stopping a recursion, just in case something else goes wrong.
As you may have noted, there are three basic parts to the kind of recursive functions you have learned about.
A recursive case in which the function calls itself with a simpler
or smaller parameter. The recursive case is typically composed of a
simplification and a combination. For sum
, the recursive case was
the call to sum
on the cdr
of the list, adding the result to the
car
of the list. Thus, cdr
was the simplification and +
was the
combination.
A combination process which takes the result of the recursive
call (and, perhaps, other data) and uses it in computing a final result.
For the sum
procedure, this process was adding the car to the recursive
result.
A base case in which the function does not call itself. For sum
,
this was simply the value 0. In writing other recursion procedures,
you may find that you need to do more computation in the base case.
A condition (alternately a base-case check or base-case guard)
that you use to decide which case holds. For sum
, the condition was whether
or not the list we want to sum is empty.
You’ll need to consider each of these parts for each recursive function you write.
Often the computation for a non-empty list involves making another test. Suppose, for instance, that we want to define a procedure that takes a list of values and selects only the numbers from that list. We can use direct recursion to develop such a procedure.
If the given list is empty, there are no elements to remove and also no elements to keep, so the correct result is the empty list.
If the given list is not empty, we examine its car and its cdr. We can use a call to the very procedure that we’re defining to select numbers in the cdr. That gives a list comprising all of the numbers.
If the car of the given list—that is, its first element–is not a number, we ignore the car and just return the result of the recursive procedure call, without change.
Otherwise, we invoke cons
to attach the car to the new list.
Translating this algorithm into Scheme yields the following definition:
;;; Procedure:
;;; select-numbers
;;; Parameters:
;;; values, a list of Scheme values
;;; Purpose:
;;; Create a new list values which consists of only the numbers
;;; in values.
;;; Produces:
;;; nums, a list of Scheme values
;;; Preconditions:
;;; [No additional]
;;; Postconditions:
;;; * number? holds for every value in nums.
;;; * Every element of values for which number? holds appears in nums.
;;; * Every element of nums appears in values.
;;; * If a number appears multiple times in either list, it appears
;;; the same number of times in both lists.
;;; * The values retain their order.
(define select-numbers
(lambda (values)
(cond
[(null? values)
null]
[(not (number? (car values)))
(select-numbers (cdr values))]
[else
(cons (car values) (select-numbers (cdr values)))])))
Apply the common elements of a recursive procedure outlined above to the
procedure select-numbers
, defined above, by filling in the blanks.
The base case condition is ________________, which checks whether ________________.
The base case value is ______________________.
The recursive case(s) are ______________________ and ______________________.
The simplifying process is _____________, which _________________, thereby giving us a “simpler value”.
Finally, the combination process is ___________________, which _____________________.