Building Abstractions with Procedures
1.3 Formulating Abstractions with Higher-Order Procedureswith Higher-Order Procedures
1.3.4 Procedures as Returned Values
e above examples demonstrate how the ability to pass procedures as arguments significantly enhances the expressive power of our program-ming language. We can achieve even more expressive power by creating procedures whose returned values are themselves procedures.
We can illustrate this idea by looking again at the fixed-point exam-ple described at the end ofSection 1.3.3. We formulated a new version of the square-root procedure as a fixed-point search, starting with the observation that√
x is a fixed-point of the function y7→ x/y. en we used average damping to make the approximations converge. Average damping is a useful general technique in itself. Namely, given a function f, we consider the function whose value at x is equal to the average of x and f (x).
We can express the idea of average damping by means of the fol-lowing procedure:
(define (average-damp f)
(lambda (x) (average x (f x))))
average-dampis a procedure that takes as its argument a procedure
fand returns as its value a procedure (produced by thelambda) that, when applied to a numberx, produces the average ofxand(f x). For example, applyingaverage-dampto thesquareprocedure produces a procedure whose value at some number x is the average of x and x2. Applying this resulting procedure to 10 returns the average of 10 and 100, or 55:59
59Observe that this is a combination whose operator is itself a combination.Exercise 1.4already demonstrated the ability to form such combinations, but that was only a toy example. Here we begin to see the real need for such combinations—when applying a procedure that is obtained as the value returned by a higher-order procedure.
((average-damp square) 10) 55
Usingaverage-damp, we can reformulate the square-root procedure as follows:
(define (sqrt x)
(fixed-point (average-damp (lambda (y) (/ x y))) 1.0))
Notice how this formulation makes explicit the three ideas in the method:
fixed-point search, average damping, and the function y7→ x/y. It is in-structive to compare this formulation of the square-root method with the original version given inSection 1.1.7. Bear in mind that these pro-cedures express the same process, and notice how much clearer the idea becomes when we express the process in terms of these abstractions. In general, there are many ways to formulate a process as a procedure. Ex-perienced programmers know how to choose procedural formulations that are particularly perspicuous, and where useful elements of the pro-cess are exposed as separate entities that can be reused in other appli-cations. As a simple example of reuse, notice that the cube root of x is a fixed point of the function y7→ x/y2, so we can immediately generalize our square-root procedure to one that extracts cube roots:60
(define (cube-root x)
(fixed-point (average-damp (lambda (y) (/ x (square y)))) 1.0))
Newton’s method
When we first introduced the square-root procedure, inSection 1.1.7, we mentioned that this was a special case of Newton’s method. If x 7→ д(x)
60SeeExercise 1.45for a further generalization.
is a differentiable function, then a solution of the equation д(x)= 0 is a fixed point of the function x 7→ f (x), where
f (x )= x − д(x ) Dд(x )
and Dд(x) is the derivative of д evaluated at x. Newton’s method is the use of the fixed-point method we saw above to approximate a solution of the equation by finding a fixed point of the function f.61
For many functions д and for sufficiently good initial guesses for x, Newton’s method converges very rapidly to a solution of д(x)= 0.62
In order to implement Newton’s method as a procedure, we must first express the idea of derivative. Note that “derivative,” like average damping, is something that transforms a function into another function.
For instance, the derivative of the function x 7→ x3is the function x 7→
3x2. In general, if д is a function and dx is a small number, then the derivative Dд of д is the function whose value at any number x is given (in the limit of small dx) by
Dд(x )= д(x+ dx)− д(x)
dx .
us, we can express the idea of derivative (taking dx to be, say, 0.00001) as the procedure
(define (deriv g)
(lambda (x) (/ (- (g (+ x dx)) (g x)) dx)))
61Elementary calculus books usually describe Newton’s method in terms of the se-quence of approximations xn+1= xn−д(xn)/Dд(xn). Having language for talking about processes and using the idea of fixed points simplifies the description of the method.
62Newton’s method does not always converge to an answer, but it can be shown that in favorable cases each iteration doubles the number-of-digits accuracy of the ap-proximation to the solution. In such cases, Newton’s method will converge much more rapidly than the half-interval method.
along with the definition
(define dx 0.00001)
Likeaverage-damp,derivis a procedure that takes a procedure as ar-gument and returns a procedure as value. For example, to approximate the derivative of x 7→ x3at 5 (whose exact value is 75) we can evaluate
(define (cube x) (* x x x)) ((deriv cube) 5)
75.00014999664018
With the aid ofderiv, we can express Newton’s method as a fixed-point process:
(define (newton-transform g)
(lambda (x) (- x (/ (g x) ((deriv g) x))))) (define (newtons-method g guess)
(fixed-point (newton-transform g) guess))
enewton-transformprocedure expresses the formula at the begin-ning of this section, andnewtons-methodis readily defined in terms of this. It takes as arguments a procedure that computes the function for which we want to find a zero, together with an initial guess. For in-stance, to find the square root of x, we can use Newton’s method to find a zero of the function y7→ y2− x starting with an initial guess of 1.63
is provides yet another form of the square-root procedure:
(define (sqrt x) (newtons-method
(lambda (y) (- (square y) x)) 1.0))
63For finding square roots, Newton’s method converges rapidly to the correct solu-tion from any starting point.
Abstractions and first-class procedures
We’ve seen two ways to express the square-root computation as an in-stance of a more general method, once as a fixed-point search and once using Newton’s method. Since Newton’s method was itself expressed as a fixed-point process, we actually saw two ways to compute square roots as fixed points. Each method begins with a function and finds a fixed point of some transformation of the function. We can express this general idea itself as a procedure:
(define (fixed-point-of-transform g transform guess) (fixed-point (transform g) guess))
is very general procedure takes as its arguments a proceduregthat computes some function, a procedure that transformsg, and an initial guess. e returned result is a fixed point of the transformed function.
Using this abstraction, we can recast the first square-root computa-tion from this seccomputa-tion (where we look for a fixed point of the average-damped version of y7→ x/y) as an instance of this general method:
(define (sqrt x)
(fixed-point-of-transform
(lambda (y) (/ x y)) average-damp 1.0))
Similarly, we can express the second square-root computation from this section (an instance of Newton’s method that finds a fixed point of the Newton transform of y7→ y2− x) as
(define (sqrt x)
(fixed-point-of-transform
(lambda (y) (- (square y) x)) newton-transform 1.0))
We beganSection 1.3with the observation that compound procedures are a crucial abstraction mechanism, because they permit us to express general methods of computing as explicit elements in our programming
language. Now we’ve seen how higher-order procedures permit us to manipulate these general methods to create further abstractions.
As programmers, we should be alert to opportunities to identify the underlying abstractions in our programs and to build upon them and generalize them to create more powerful abstractions. is is not to say that one should always write programs in the most abstract way possible; expert programmers know how to choose the level of abstrac-tion appropriate to their task. But it is important to be able to think in terms of these abstractions, so that we can be ready to apply them in new contexts. e significance of higher-order procedures is that they enable us to represent these abstractions explicitly as elements in our programming language, so that they can be handled just like other com-putational elements.
In general, programming languages impose restrictions on the ways in which computational elements can be manipulated. Elements with the fewest restrictions are said to have first-class status. Some of the
“rights and privileges” of first-class elements are:64
• ey may be named by variables.
• ey may be passed as arguments to procedures.
• ey may be returned as the results of procedures.
• ey may be included in data structures.65
Lisp, unlike other common programming languages, awards procedures full first-class status. is poses challenges for efficient implementation,
64e notion of first-class status of programming-language elements is due to the British computer scientist Christopher Strachey (1916-1975).
65We’ll see examples of this aer we introduce data structures inChapter 2.
but the resulting gain in expressive power is enormous.66 Exercise 1.40:Define a procedurecubicthat can be used together with thenewtons-methodprocedure in expressions of the form
(newtons-method (cubic a b c) 1)
to approximate zeros of the cubic x3+ ax2+ bx + c.
Exercise 1.41:Define a proceduredoublethat takes a pro-cedure of one argument as argument and returns a proce-dure that applies the original proceproce-dure twice. For exam-ple, ifincis a procedure that adds 1 to its argument, then
(double inc) should be a procedure that adds 2. What value is returned by
(((double (double double)) inc) 5)
Exercise 1.42:Let f and д be two one-argument functions.
e composition f aer д is defined to be the function x 7→
f (д(x )). Define a procedurecomposethat implements com-position. For example, if incis a procedure that adds 1 to its argument,
((compose square inc) 6) 49
66e major implementation cost of first-class procedures is that allowing procedures to be returned as values requires reserving storage for a procedure’s free variables even while the procedure is not executing. In the Scheme implementation we will study in Section 4.1, these variables are stored in the procedure’s environment.
Exercise 1.43:If f is a numerical function and n is a posi-tive integer, then we can form the nth repeated application of f , which is defined to be the function whose value at x is f (f (. . . (f (x)) . . . )). For example, if f is the function x 7→ x + 1, then the nth repeated application of f is the function x 7→ x +n. If f is the operation of squaring a num-ber, then the nth repeated application of f is the function that raises its argument to the 2n-th power. Write a proce-dure that takes as inputs a proceproce-dure that computes f and a positive integer n and returns the procedure that computes the nthrepeated application of f . Your procedure should be able to be used as follows:
((repeated square 2) 5) 625
Hint: You may find it convenient to usecomposefrom Ex-ercise 1.42.
Exercise 1.44:e idea of smoothing a function is an im-portant concept in signal processing. If f is a function and dxis some small number, then the smoothed version of f is the function whose value at a point x is the average of f (x− dx ), f (x), and f (x +dx). Write a proceduresmooththat takes as input a procedure that computes f and returns a proce-dure that computes the smoothed f . It is sometimes valu-able to repeatedly smooth a function (that is, smooth the smoothed function, and so on) to obtain the n-fold smoothed function. Show how to generate the n-fold smoothed func-tion of any given funcfunc-tion usingsmoothandrepeatedfrom Exercise 1.43.
Exercise 1.45:We saw inSection 1.3.3that aempting to compute square roots by naively finding a fixed point of y 7→ x/y does not converge, and that this can be fixed by average damping. e same method works for finding cube roots as fixed points of the average-damped y7→ x/y2. Un-fortunately, the process does not work for fourth roots—a single average damp is not enough to make a fixed-point search for y 7→ x/y3 converge. On the other hand, if we average damp twice (i.e., use the average damp of the av-erage damp of y7→ x/y3) the fixed-point search does con-verge. Do some experiments to determine how many av-erage damps are required to compute nth roots as a fixed-point search based upon repeated average damping of y7→
x/yn−1. Use this to implement a simple procedure for com-puting nthroots usingfixed-point,average-damp, and the
repeatedprocedure ofExercise 1.43. Assume that any arith-metic operations you need are available as primitives.
Exercise 1.46:Several of the numerical methods described in this chapter are instances of an extremely general com-putational strategy known as iterative improvement. Itera-tive improvement says that, to compute something, we start with an initial guess for the answer, test if the guess is good enough, and otherwise improve the guess and continue the process using the improved guess as the new guess. Write a procedureiterative-improvethat takes two procedures as arguments: a method for telling whether a guess is good enough and a method for improving a guess.
iterative-improveshould return as its value a procedure that takes a guess as argument and keeps improving the guess until it is
good enough. Rewrite thesqrtprocedure ofSection 1.1.7 and thefixed-pointprocedure ofSection 1.3.3in terms of
iterative-improve.