Building Abstractions with Procedures
1.2 Procedures and the Processes They Generate
1.2.1 Linear Recursion and Iteration
We begin by considering the factorial function, defined by n!= n · (n − 1) · (n − 2) · · · 3 · 2 · 1.
ere are many ways to compute factorials. One way is to make use of the observation that n! is equal to n times (n− 1)! for any positive integer n:
n!= n · [(n − 1) · (n − 2) · · · 3 · 2 · 1] = n · (n − 1)!.
us, we can compute n! by computing (n − 1)! and multiplying the result by n. If we add the stipulation that 1! is equal to 1, this observation translates directly into a procedure:
(define (factorial n) (if (= n 1)
1
(* n (factorial (- n 1)))))
(factorial 6) (* 6 (factorial 5)) (* 6 (* 5 (factorial 4))) (* 6 (* 5 (* 4 (factorial 3)))) (* 6 (* 5 (* 4 (* 3 (factorial 2))))) (* 6 (* 5 (* 4 (* 3 (* 2 (factorial 1)))))) (* 6 (* 5 (* 4 (* 3 (* 2 1)))))
(* 6 (* 5 (* 4 (* 3 2)))) (* 6 (* 5 (* 4 6))) (* 6 (* 5 24)) (* 6 120) 720
Figure 1.3:A linear recursive process for computing 6!.
We can use the substitution model ofSection 1.1.5to watch this proce-dure in action computing 6!, as shown inFigure 1.3.
Now let’s take a different perspective on computing factorials. We could describe a rule for computing n! by specifying that we first mul-tiply 1 by 2, then mulmul-tiply the result by 3, then by 4, and so on until we reach n. More formally, we maintain a running product, together with a counter that counts from 1 up to n. We can describe the computation by saying that the counter and the product simultaneously change from one step to the next according to the rule
product ← counter * product counter ← counter + 1
and stipulating that n! is the value of the product when the counter exceeds n.
Once again, we can recast our description as a procedure for com-puting factorials:29
29In a real program we would probably use the block structure introduced in the last
(factorial 6) (fact-iter 1 1 6) (fact-iter 1 2 6) (fact-iter 2 3 6) (fact-iter 6 4 6) (fact-iter 24 5 6) (fact-iter 120 6 6) (fact-iter 720 7 6) 720
Figure 1.4:A linear iterative process for computing 6!.
(define (factorial n) (fact-iter 1 1 n))
(define (fact-iter product counter max-count) (if (> counter max-count)
product
(fact-iter (* counter product) (+ counter 1) max-count)))
As before, we can use the substitution model to visualize the process of computing 6!, as shown inFigure 1.4.
(define (factorial n)
(define (iter product counter) (if (> counter n)
product
(iter (* counter product) (+ counter 1)))) (iter 1 1))
We avoided doing this here so as to minimize the number of things to think about at once.
Compare the two processes. From one point of view, they seem hardly different at all. Both compute the same mathematical function on the same domain, and each requires a number of steps proportional to n to compute n!. Indeed, both processes even carry out the same sequence of multiplications, obtaining the same sequence of partial products. On the other hand, when we consider the “shapes” of the two processes, we find that they evolve quite differently.
Consider the first process. e substitution model reveals a shape of expansion followed by contraction, indicated by the arrow inFigure 1.3.
e expansion occurs as the process builds up a chain of deferred oper-ations(in this case, a chain of multiplications). e contraction occurs as the operations are actually performed. is type of process, charac-terized by a chain of deferred operations, is called a recursive process.
Carrying out this process requires that the interpreter keep track of the operations to be performed later on. In the computation of n!, the length of the chain of deferred multiplications, and hence the amount of infor-mation needed to keep track of it, grows linearly with n (is proportional to n), just like the number of steps. Such a process is called a linear re-cursive process.
By contrast, the second process does not grow and shrink. At each step, all we need to keep track of, for any n, are the current values of the variablesproduct,counter, andmax-count. We call this an iterative process. In general, an iterative process is one whose state can be sum-marized by a fixed number of state variables, together with a fixed rule that describes how the state variables should be updated as the process moves from state to state and an (optional) end test that specifies con-ditions under which the process should terminate. In computing n!, the number of steps required grows linearly with n. Such a process is called a linear iterative process.
e contrast between the two processes can be seen in another way.
In the iterative case, the program variables provide a complete descrip-tion of the state of the process at any point. If we stopped the compu-tation between steps, all we would need to do to resume the computa-tion is to supply the interpreter with the values of the three program variables. Not so with the recursive process. In this case there is some additional “hidden” information, maintained by the interpreter and not contained in the program variables, which indicates “where the process is” in negotiating the chain of deferred operations. e longer the chain, the more information must be maintained.30
In contrasting iteration and recursion, we must be careful not to confuse the notion of a recursive process with the notion of a recursive procedure. When we describe a procedure as recursive, we are referring to the syntactic fact that the procedure definition refers (either directly or indirectly) to the procedure itself. But when we describe a process as following a paern that is, say, linearly recursive, we are speaking about how the process evolves, not about the syntax of how a procedure is wrien. It may seem disturbing that we refer to a recursive procedure such asfact-iteras generating an iterative process. However, the pro-cess really is iterative: Its state is captured completely by its three state variables, and an interpreter need keep track of only three variables in order to execute the process.
One reason that the distinction between process and procedure may be confusing is that most implementations of common languages (in-cluding Ada, Pascal, and C) are designed in such a way that the interpre-tation of any recursive procedure consumes an amount of memory that
30When we discuss the implementation of procedures on register machines in Chap-ter 5, we will see that any iChap-terative process can be realized “in hardware” as a machine that has a fixed set of registers and no auxiliary memory. In contrast, realizing a re-cursive process requires a machine that uses an auxiliary data structure known as a
grows with the number of procedure calls, even when the process de-scribed is, in principle, iterative. As a consequence, these languages can describe iterative processes only by resorting to special-purpose “loop-ing constructs” such asdo,repeat,until,for, andwhile. e imple-mentation of Scheme we shall consider inChapter 5does not share this defect. It will execute an iterative process in constant space, even if the iterative process is described by a recursive procedure. An implemen-tation with this property is called tail-recursive. With a tail-recursive implementation, iteration can be expressed using the ordinary proce-dure call mechanism, so that special iteration constructs are useful only as syntactic sugar.31
Exercise 1.9:Each of the following two procedures defines a method for adding two positive integers in terms of the proceduresinc, which increments its argument by 1, and
dec, which decrements its argument by 1.
(define (+ a b)
(if (= a 0) b (inc (+ (dec a) b)))) (define (+ a b)
(if (= a 0) b (+ (dec a) (inc b))))
Using the substitution model, illustrate the process gener-ated by each procedure in evaluating(+ 4 5). Are these processes iterative or recursive?
31Tail recursion has long been known as a compiler optimization trick. A coherent semantic basis for tail recursion was provided by CarlHewi (1977), who explained it in terms of the “message-passing” model of computation that we shall discuss inChapter 3. Inspired by this, Gerald Jay Sussman and Guy Lewis Steele Jr. (seeSteele and Sussman 1975) constructed a tail-recursive interpreter for Scheme. Steele later showed how tail recursion is a consequence of the natural way to compile procedure calls (Steele 1977).
e standard for Scheme requires that Scheme implementations be tail-recursive.
Exercise 1.10:e following procedure computes a math-ematical function called Ackermann’s function.
(define (A x y) (cond ((= y 0) 0)
((= x 0) (* 2 y)) ((= y 1) 2)
(else (A (- x 1) (A x (- y 1))))))
What are the values of the following expressions?
(A 1 10) (A 2 4) (A 3 3)
Consider the following procedures, whereAis the proce-dure defined above:
(define (f n) (A 0 n)) (define (g n) (A 1 n)) (define (h n) (A 2 n)) (define (k n) (* 5 n n))
Give concise mathematical definitions for the functions com-puted by the proceduresf,g, andhfor positive integer val-ues of n. For example,(k n)computes 5n2.