Like lists, natural numbers have a recursive structure of which we can take advantage when we write direct-recursion procedures. A natural number is either (a) zero, or (b) the successor of a smaller natural number, which we can obtain by subtracting 1.
Standard Scheme provides the predicate zero? to distinguish between
the (a) and (b) cases, so we can again use an if-expression to
ensure that only the expression for the appropriate case is evaluated. So
we can write a procedure that applies to any natural number if we
know (a) what value it should return when the argument is 0 and (b) how to
convert the value that the procedure would return for the next smaller
natural number into the appropriate return value for a given non-zero
natural number.
For instance, let's consider a procedure that computes the termial of any natural number number, that is, the result of adding
together all of the natural numbers up to and including number.
Here are some illustrative sample calls:
> (termial 7) ; = 7 + 6 + 5 + 4 + 3 + 2 + 1 + 0 28 > (termial 1) ; = 1 + 0 1 > (termial 0) 0
And here's how I'd define termial:
;;; termial: compute the sum of natural numbers not greater ;;; than a given natural number ;;; Given: ;;; NUMBER, an integer. ;;; Result: ;;; SUM, an integer. ;;; Precondition: ;;; NUMBER is exact and non-negative (in other words, ;;; a natural number). ;;; Postcondition: ;;; SUM is the sum of the natural numbers not greater ;;; than NUMBER. (define termial (lambda (number) (if (zero? number) 0 (+ number (termial (- number 1))))))
Whereas in a list recursion, we called
the cdr procedure to reduce the length of the list in making the
recursive call, the operation that we apply in recursion with natural
numbers reduces the natural-number argument in the recursive call by 1.
Here's a summary of what actually happens during the evaluation of a call
to the termial procedure -- say, (termial 5):
(termial 5)
--> (+ 5 (termial 4))
--> (+ 5 (+ 4 (termial 3)))
--> (+ 5 (+ 4 (+ 3 (termial 2))))
--> (+ 5 (+ 4 (+ 3 (+ 2 (termial 1)))))
--> (+ 5 (+ 4 (+ 3 (+ 2 (+ 1 (termial 0))))))
--> (+ 5 (+ 4 (+ 3 (+ 2 (+ 1 0)))))
--> (+ 5 (+ 4 (+ 3 (+ 2 1))))
--> (+ 5 (+ 4 (+ 3 3)))
--> (+ 5 (+ 4 6))
--> (+ 5 10)
--> 15
The restriction that termial takes only natural numbers as arguments
is an important one. If we gave it a negative number or a non-integer,
we'd have a runaway recursion, because we cannot get to 0 by subtracting 1
repeatedly from a negative number or from a non-integer, and so the base
case would never be reached. If we gave the termial procedure an
approximation rather than an exact number, we might or might not be able to
reach zero, depending on how accurate the approximation is and how much of
that accuracy is preserved by the subtraction procedure.
The important part of getting recursion to work is making sure that the base case is inevitably reached by performing the simplification operation enough times. For instance, we can use direct recursion on exact positive integers with 1, rather than 0, as the base case, provided that we always start from an exact integer greater than or equal to 1.
As an illustration of this, consider the factorial procedure,
which finds the product of the positive integers up to and including its
argument. (The computation would be pointless if we included 0 as one of
the factors, since 0 times anything is 0.) Again, we can begin by writing
test cases:
> (factorial 7) ; = 7 x 6 x 5 x 4 x 3 x 2 x 1 5040 > (factorial 2) ; = 2 x 1 2 > (factorial 1) 1
Here is the definition:
;;; factorial: compute the product of positive integers not ;;; greater than a given positive integer ;;; Given: ;;; NUMBER, an integer. ;;; Result: ;;; PRODUCT, an integer. ;;; Precondition: ;;; NUMBER is positive and exact. ;;; Postcondition: ;;; PRODUCT is the product of the positive integers not ;;; greater than NUMBER. (define factorial (lambda (number) (if (= number 1) 1 (* number (factorial (- number 1))))))
Similarly, we can use direct recursion to approach the base case from below
by repeated additions of 1, if we know that our starting point is less than
or equal to that base case and that the difference between them is an exact
integer. As an example, let's develop a procedure that takes two exact
integers, lower and upper, as arguments, and returns a list
of the exact integers from lower to upper, inclusive, in ascending order,
thus:
> (count-from 1 10) (1 2 3 4 5 6 7 8 9 10) > (count-from -3 +4) (-3 -2 -1 0 1 2 3 4) > (count-from 116 117) (116 117) > (count-from -38 -38) (-38)
Here is the definition:
;;; count-from: given two natural numbers, construct ;;; a list of the natural numbers from the first to ;;; the second, inclusive, in ascending order ;;; Given: ;;; LOWER and UPPER, both integers. ;;; Result: ;;; LS, a list. ;;; Preconditions: ;;; (1) LOWER and UPPER are exact. ;;; (2) LOWER is less than or equal to UPPER. ;;; Postconditions: ;;; (1) The length of LS is UPPER - LOWER + 1. ;;; (2) For every natural number k less than the length of ;;; LS, the element in position k of LS is LOWER + k. (define count-from (lambda (lower upper) (if (= lower upper) (list upper) (cons lower (count-from (+ lower 1) upper)))))