We have seen that procedures are, in effect, abstractions that describe compound operations on numbers independent of the particular numbers. For example, when we
(define (cube x) (* x x x))
we are not talking about the cube of a particular number, but rather about a method for obtaining the cube of any number. Of course we could get along without ever defining this procedure, by always writing expressions such as
(* 3 3 3)
(* x x x)
(* y y y)
and never mentioning cube
explicitly. This would place us at a serious disadvantage, forcing us to work always at the level of the particular operations that happen to be primitives in the language (multiplication, in this case) rather than in terms of higher-level operations. Our programs would be able to compute cubes, but our language would lack the ability to express the concept of cubing. One of the things we should demand from a powerful programming language is the ability to build abstractions by assigning names to common patterns and then to work in terms of the abstractions directly. Procedures provide this ability. This is why all but the most primitive programming languages include mechanisms for defining procedures.
Yet even in numerical processing we will be severely limited in our ability to create abstractions if we are restricted to procedures whose parameters must be numbers. Often the same programming pattern will be used with a number of different procedures. To express such patterns as concepts, we will need to construct procedures that can accept procedures as arguments or return procedures as values. Procedures that manipulate procedures are called higher-order procedures. This section shows how higher-order procedures can serve as powerful abstraction mechanisms, vastly increasing the expressive power of our language.
Consider the following three procedures. The first computes the sum of the integers from a
through b
:
(define (sum-integers a b)
(if (> a b)
0
(+ a (sum-integers (+ a 1) b))))
The second computes the sum of the cubes of the integers in the given range:
(define (sum-cubes a b)
(if (> a b)
0
(+ (cube a)
(sum-cubes (+ a 1) b))))
The third computes the sum of a sequence of terms in the series
which converges to
(define (pi-sum a b)
(if (> a b)
0
(+ (/ 1.0 (* a (+ a 2)))
(pi-sum (+ a 4) b))))
These three procedures clearly share a common underlying pattern. They are for the most part identical, differing only in the name of the procedure, the function of a
used to compute the term to be added, and the function that provides the next value of a
. We could generate each of the procedures by filling in slots in the same template:
(define (⟨name⟩ a b)
(if (> a b)
0
(+ (⟨term⟩ a)
(⟨name⟩ (⟨next⟩ a) b))))
The presence of such a common pattern is strong evidence that there is a useful abstraction waiting to be brought to the surface. Indeed, mathematicians long ago identified the abstraction of summation of a series and invented “sigma notation,” for example
to express this concept. The power of sigma notation is that it allows mathematicians to deal with the concept of summation itself rather than only with particular sums—for example, to formulate general results about sums that are independent of the particular series being summed.
Similarly, as program designers, we would like our language to be powerful enough so that we can write a procedure that expresses the concept of summation itself rather than only procedures that compute particular sums. We can do so readily in our procedural language by taking the common template shown above and transforming the “slots” into formal parameters:
(define (sum term a next b)
(if (> a b)
0
(+ (term a)
(sum term (next a) next b))))
Notice that sum
takes as its arguments the lower and upper bounds a
and b
together with the procedures term
and next
. We can use sum
just as we would any procedure. For example, we can use it (along with a procedure inc
that increments its argument by 1) to define sum-cubes
:
(define (inc n) (+ n 1))
(define (sum-cubes a b)
(sum cube a inc b))
Using this, we can compute the sum of the cubes of the integers from 1 to 10:
(sum-cubes 1 10)
3025
With the aid of an identity procedure to compute the term, we can define sum-integers
in terms of sum
:
(define (identity x) x)
(define (sum-integers a b)
(sum identity a inc b))
Then we can add up the integers from 1 to 10:
(sum-integers 1 10)
55
We can also define pi-sum
in the same way:^[Notice that we have used block structure (1.1.8) to embed the definitions of pi-next
and pi-term
within pi-sum
, since these procedures are unlikely to be useful for any other purpose. We will see how to get rid of them altogether in 1.3.2.]
(define (pi-sum a b)
(define (pi-term x)
(/ 1.0 (* x (+ x 2))))
(define (pi-next x)
(+ x 4))
(sum pi-term a pi-next b))
Using these procedures, we can compute an approximation to
(* 8 (pi-sum 1 1000))
3.139592655589783
Once we have sum
, we can use it as a building block in formulating further concepts. For instance, the definite integral of a function
for small values of
(define (integral f a b dx)
(define (add-dx x) (+ x dx))
(* (sum f (+ a (/ dx 2.0)) add-dx b)
dx))
(integral cube 0 1 0.01)
.24998750000000042
(integral cube 0 1 0.001)
.249999875000001
(The exact value of the integral of cube
between 0 and 1 is 1/4.)
Exercise 1.29: Simpson’s Rule is a more accurate method of numerical integration than the method illustrated above. Using Simpson’s Rule, the integral of a function
where cube
between 0 and 1 (with integral
procedure shown above.
Exercise 1.30: The sum
procedure above generates a linear recursion. The procedure can be rewritten so that the sum is performed iteratively. Show how to do this by filling in the missing expressions in the following definition:
(define (sum term a next b)
(define (iter a result)
(if ⟨??⟩
⟨??⟩
(iter ⟨??⟩ ⟨??⟩)))
(iter ⟨??⟩ ⟨??⟩))
Exercise 1.31:
- The
sum
procedure is only the simplest of a vast number of similar abstractions that can be captured as higher-order procedures.^[The intent of Exercise 1.31 through Exercise 1.33 is to demonstrate the expressive power that is attained by using an appropriate abstraction to consolidate many seemingly disparate operations. However, though accumulation and filtering are elegant ideas, our hands are somewhat tied in using them at this point since we do not yet have data structures to provide suitable means of combination for these abstractions. We will return to these ideas in 2.2.3 when we show how to use sequences as interfaces for combining filters and accumulators to build even more powerful abstractions. We will see there how these methods really come into their own as a powerful and elegant approach to designing programs.] Write an analogous procedure calledproduct
that returns the product of the values of a function at points over a given range. Show how to definefactorial
in terms ofproduct
. Also useproduct
to compute approximations to$\pi$ using the formula^[This formula was discovered by the seventeenth-century English mathematician John Wallis.]
- If your
product
procedure generates a recursive process, write one that generates an iterative process. If it generates an iterative process, write one that generates a recursive process.
Exercise 1.32:
-
Show that
sum
andproduct
(Exercise 1.31) are both special cases of a still more general notion calledaccumulate
that combines a collection of terms, using some general accumulation function:(accumulate combiner null-value term a next b)
Accumulate
takes as arguments the same term and range specifications assum
andproduct
, together with acombiner
procedure (of two arguments) that specifies how the current term is to be combined with the accumulation of the preceding terms and anull-value
that specifies what base value to use when the terms run out. Writeaccumulate
and show howsum
andproduct
can both be defined as simple calls toaccumulate
. -
If your
accumulate
procedure generates a recursive process, write one that generates an iterative process. If it generates an iterative process, write one that generates a recursive process.
Exercise 1.33: You can obtain an even more general version of accumulate
(Exercise 1.32) by introducing the notion of a filter on the terms to be combined. That is, combine only those terms derived from values in the range that satisfy a specified condition. The resulting filtered-accumulate
abstraction takes the same arguments as accumulate, together with an additional predicate of one argument that specifies the filter. Write filtered-accumulate
as a procedure. Show how to express the following using filtered-accumulate
:
- the sum of the squares of the prime numbers in the interval
$a$ to$b$ (assuming that you have aprime?
predicate already written) - the product of all the positive integers less than
$n$ that are relatively prime to$n$ (i.e., all positive integers$i < n$ such that$\text{GCD}(i,n) = 1$ ).
In using sum
as in 1.3.1, it seems terribly awkward to have to define trivial procedures such as pi-term
and pi-next
just so we can use them as arguments to our higher-order procedure. Rather than define pi-next
and pi-term
, it would be more convenient to have a way to directly specify “the procedure that returns its input incremented by 4” and “the procedure that returns the reciprocal of its input times its input plus 2.” We can do this by introducing the special form lambda
, which creates procedures. Using lambda
we can describe what we want as
(lambda (x) (+ x 4))
and
(lambda (x) (/ 1.0 (* x (+ x 2))))
Then our pi-sum
procedure can be expressed without defining any auxiliary procedures as
(define (pi-sum a b)
(sum (lambda (x) (/ 1.0 (* x (+ x 2))))
a
(lambda (x) (+ x 4))
b))
Again using lambda
, we can write the integral
procedure without having to define the auxiliary procedure add-dx
:
(define (integral f a b dx)
(* (sum f (+ a (/ dx 2.0))
(lambda (x) (+ x dx))
b)
dx))
In general, lambda
is used to create procedures in the same way as define
, except that no name is specified for the procedure:
(lambda (⟨formal-parameters⟩) ⟨body⟩)
The resulting procedure is just as much a procedure as one that is created using define
. The only difference is that it has not been associated with any name in the environment. In fact,
(define (plus4 x) (+ x 4))
is equivalent to
(define plus4 (lambda (x) (+ x 4)))
We can read a lambda
expression as follows:
(lambda (x) (+ x 4))
| | | | |
the procedure of an argument x that adds x and 4
Like any expression that has a procedure as its value, a lambda
expression can be used as the operator in a combination such as
((lambda (x y z) (+ x y (square z))) 1 2 3)
12
or, more generally, in any context where we would normally use a procedure name.^[It would be clearer and less intimidating to people learning Lisp if a name more obvious than lambda
, such as make-procedure
, were used. But the convention is firmly entrenched. The notation is adopted from the λ-calculus, a mathematical formalism introduced by the mathematical logician Alonzo Church (1941). Church developed the λ-calculus to provide a rigorous foundation for studying the notions of function and function application. The λ-calculus has become a basic tool for mathematical investigations of the semantics of programming languages.]
Another use of lambda
is in creating local variables. We often need local variables in our procedures other than those that have been bound as formal parameters. For example, suppose we wish to compute the function
which we could also express as
In writing a procedure to compute
(define (f x y)
(define (f-helper a b)
(+ (* x (square a))
(* y b)
(* a b)))
(f-helper (+ 1 (* x y))
(- 1 y)))
Of course, we could use a lambda
expression to specify an anonymous procedure for binding our local variables. The body of f
then becomes a single call to that procedure:
(define (f x y)
((lambda (a b)
(+ (* x (square a))
(* y b)
(* a b)))
(+ 1 (* x y))
(- 1 y)))
This construct is so useful that there is a special form called let
to make its use more convenient. Using let
, the f
procedure could be written as
(define (f x y)
(let ((a (+ 1 (* x y)))
(b (- 1 y)))
(+ (* x (square a))
(* y b)
(* a b))))
The general form of a let
expression is
(let ((⟨var₁⟩ ⟨exp₁⟩)
(⟨var₂⟩ ⟨exp₂⟩)
…
(⟨varₙ⟩ ⟨expₙ⟩))
⟨body⟩)
which can be thought of as saying
let ⟨var₁⟩ have the value ⟨exp₁⟩ and
⟨var₂⟩ have the value ⟨exp₂⟩ and
…
⟨varₙ⟩ have the value ⟨expₙ⟩
in ⟨body⟩
The first part of the let
expression is a list of name-expression pairs. When the let
is evaluated, each name is associated with the value of the corresponding expression. The body of the let
is evaluated with these names bound as local variables. The way this happens is that the let
expression is interpreted as an alternate syntax for
((lambda (⟨var₁⟩ … ⟨varₙ⟩)
⟨body⟩)
⟨exp₁⟩
…
⟨expₙ⟩)
No new mechanism is required in the interpreter in order to provide local variables. A let
expression is simply syntactic sugar for the underlying lambda
application.
We can see from this equivalence that the scope of a variable specified by a let
expression is the body of the let
. This implies that:
-
Let
allows one to bind variables as locally as possible to where they are to be used. For example, if the value ofx
is 5, the value of the expression(+ (let ((x 3)) (+ x (* x 10))) x)
is 38. Here, the
x
in the body of thelet
is 3, so the value of thelet
expression is 33. On the other hand, thex
that is the second argument to the outermost+
is still 5. -
The variables’ values are computed outside the
let
. This matters when the expressions that provide the values for the local variables depend upon variables having the same names as the local variables themselves. For example, if the value ofx
is 2, the expression(let ((x 3) (y (+ x 2))) (* x y))
will have the value 12 because, inside the body of the
let
,x
will be 3 andy
will be 4 (which is the outerx
plus 2).
Sometimes we can use internal definitions to get the same effect as with let
. For example, we could have defined the procedure f
above as
(define (f x y)
(define a
(+ 1 (* x y)))
(define b (- 1 y))
(+ (* x (square a))
(* y b)
(* a b)))
We prefer, however, to use let
in situations like this and to use internal define
only for internal procedures.^[Understanding internal definitions well enough to be sure a program means what we intend it to mean requires a more elaborate model of the evaluation process than we have presented in this chapter. The subtleties do not arise with internal definitions of procedures, however. We will return to this issue in 4.1.6, after we learn more about evaluation.]
Exercise 1.34: Suppose we define the procedure
(define (f g) (g 2))
Then we have
(f square)
4
(f (lambda (z) (* z (+ z 1))))
6
What happens if we (perversely) ask the interpreter to evaluate the combination (f f)
? Explain.
We introduced compound procedures in 1.1.4 as a mechanism for abstracting patterns of numerical operations so as to make them independent of the particular numbers involved. With higher-order procedures, such as the integral
procedure of 1.3.1, we began to see a more powerful kind of abstraction: procedures used to express general methods of computation, independent of the particular functions involved. In this section we discuss two more elaborate examples—general methods for finding zeros and fixed points of functions—and show how these methods can be expressed directly as procedures.
The half-interval method is a simple but powerful technique for finding roots of an equation
(define (search f neg-point pos-point)
(let ((midpoint
(average neg-point pos-point)))
(if (close-enough? neg-point pos-point)
midpoint
(let ((test-value (f midpoint)))
(cond
((positive? test-value)
(search f neg-point midpoint))
((negative? test-value)
(search f midpoint pos-point))
(else midpoint))))))
We assume that we are initially given the function
To test whether the endpoints are “close enough” we can use a procedure similar to the one used in 1.1.7 for computing square roots:^[We have used 0.001 as a representative “small” number to indicate a tolerance for the acceptable error in a calculation. The appropriate tolerance for a real calculation depends upon the problem to be solved and the limitations of the computer and the algorithm. This is often a very subtle consideration, requiring help from a numerical analyst or some other kind of magician.]
(define (close-enough? x y)
(< (abs (- x y)) 0.001))
Search
is awkward to use directly, because we can accidentally give it points at which search
via the following procedure, which checks to see which of the endpoints has a negative function value and which has a positive value, and calls the search
procedure accordingly. If the function has the same sign on the two given points, the half-interval method cannot be used, in which case the procedure signals an error.^[This can be accomplished using error
, which takes as arguments a number of items that are printed as error messages.]
(define (half-interval-method f a b)
(let ((a-value (f a))
(b-value (f b)))
(cond ((and (negative? a-value)
(positive? b-value))
(search f a b))
((and (negative? b-value)
(positive? a-value))
(search f b a))
(else
(error "Values are not of
opposite sign" a b)))))
The following example uses the half-interval method to approximate
(half-interval-method sin 2.0 4.0)
3.14111328125
Here is another example, using the half-interval method to search for a root of the equation
(half-interval-method
(lambda (x) (- (* x x x) (* 2 x) 3))
1.0
2.0)
1.89306640625
A number
until the value does not change very much. Using this idea, we can devise a procedure fixed-point
that takes as inputs a function and an initial guess and produces an approximation to a fixed point of the function. We apply the function repeatedly until we find two successive values whose difference is less than some prescribed tolerance:
(define tolerance 0.00001)
(define (fixed-point f first-guess)
(define (close-enough? v1 v2)
(< (abs (- v1 v2))
tolerance))
(define (try guess)
(let ((next (f guess)))
(if (close-enough? guess next)
next
(try next))))
(try first-guess))
For example, we can use this method to approximate the fixed point of the cosine function, starting with 1 as an initial approximation:^[Try this during a boring lecture: Set your calculator to radians mode and then repeatedly press the cos
button until you obtain the fixed point.]
(fixed-point cos 1.0)
.7390822985224023
Similarly, we can find a solution to the equation
(fixed-point (lambda (y) (+ (sin y) (cos y)))
1.0)
1.2587315962971173
The fixed-point process is reminiscent of the process we used for finding square roots in 1.1.7. Both are based on the idea of repeatedly improving a guess until the result satisfies some criterion. In fact, we can readily formulate the square-root computation as a fixed-point search. Computing the square root of some number lambda
. (lambda (y) (/ x y))
, that is, the function whose value at
(define (sqrt x)
(fixed-point (lambda (y) (/ x y))
1.0))
Unfortunately, this fixed-point search does not converge. Consider an initial guess
One way to control such oscillations is to prevent the guesses from changing so much. Since the answer is always between our guess
(define (sqrt x)
(fixed-point
(lambda (y) (average y (/ x y)))
1.0))
(Note that
With this modification, the square-root procedure works. In fact, if we unravel the definitions, we can see that the sequence of approximations to the square root generated here is precisely the same as the one generated by our original square-root procedure of 1.1.7. This approach of averaging successive approximations to a solution, a technique that we call average damping, often aids the convergence of fixed-point searches.
Exercise 1.35: Show that the golden ratio fixed-point
procedure.
Exercise 1.36: Modify fixed-point
so that it prints the sequence of approximations it generates, using the newline
and display
primitives shown in Exercise 1.22. Then find a solution to log
procedure, which computes natural logarithms.) Compare the number of steps this takes with and without average damping. (Note that you cannot start fixed-point
with a guess of 1, as this would cause division by
Exercise 1.37:
- An infinite continued fraction is an expression of the form
As an example, one can show that the infinite continued fraction expansion with the
Suppose that n
and d
are procedures of one argument (the term index cont-frac
such that evaluating (cont-frac n d k)
computes the value of the
``` {.scheme}
(cont-frac (lambda (i) 1.0)
(lambda (i) 1.0)
k)
```
for successive values of `k`. How large must you make `k` in order to get an approximation that is accurate to 4 decimal places?
- If your
cont-frac
procedure generates a recursive process, write one that generates an iterative process. If it generates an iterative process, write one that generates a recursive process.
Exercise 1.38: In 1737, the Swiss mathematician Leonhard Euler published a memoir De Fractionibus Continuis, which included a continued fraction expansion for cont-frac
procedure from Exercise 1.37 to approximate
Exercise 1.39: A continued fraction representation of the tangent function was published in 1770 by the German mathematician J.H. Lambert:
where (tan-cf x k)
that computes an approximation to the tangent function based on Lambert’s formula. k
specifies the number of terms to compute, as in Exercise 1.37.
The above examples demonstrate how the ability to pass procedures as arguments significantly enhances the expressive power of our programming 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 example described at the end of 1.3.3. We formulated a new version of the square-root procedure as a fixed-point search, starting with the observation that
We can express the idea of average damping by means of the following procedure:
(define (average-damp f)
(lambda (x)
(average x (f x))))
Average-damp
is a procedure that takes as its argument a procedure f
and returns as its value a procedure (produced by the lambda
) that, when applied to a number x
, produces the average of x
and (f x)
. For example, applying average-damp
to the square
procedure produces a procedure whose value at some number
((average-damp square) 10)
55
Using average-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
(define (cube-root x)
(fixed-point
(average-damp
(lambda (y)
(/ x (square y))))
1.0))
When we first introduced the square-root procedure, in 1.1.7, we mentioned that this was a special case of Newton’s method. If
and
For many functions
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
Thus, we can express the idea of derivative (taking
(define (deriv g)
(lambda (x)
(/ (- (g (+ x dx)) (g x))
dx)))
along with the definition
(define dx 0.00001)
Like average-damp
, deriv
is a procedure that takes a procedure as argument and returns a procedure as value. For example, to approximate the derivative of
(define (cube x) (* x x x))
((deriv cube) 5)
75.00014999664018
With the aid of deriv
, 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))
The newton-transform
procedure expresses the formula at the beginning of this section, and newtons-method
is 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 instance, to find the square root of
This provides yet another form of the square-root procedure:
(define (sqrt x)
(newtons-method
(lambda (y)
(- (square y) x))
1.0))
We’ve seen two ways to express the square-root computation as an instance 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))
This very general procedure takes as its arguments a procedure g
that computes some function, a procedure that transforms g
, and an initial guess. The returned result is a fixed point of the transformed function.
Using this abstraction, we can recast the first square-root computation from this section (where we look for a fixed point of the average-damped version of
(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
(define (sqrt x)
(fixed-point-of-transform
(lambda (y) (- (square y) x))
newton-transform
1.0))
We began section 1.3 with 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. This 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 abstraction 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. The 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 computational 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:^[The notion of first-class status of programming-language elements is due to the British computer scientist Christopher Strachey (1916-1975).]
- They may be named by variables.
- They may be passed as arguments to procedures.
- They may be returned as the results of procedures.
- They may be included in data structures.^[We’ll see examples of this after we introduce data structures in Chapter 2.]
Lisp, unlike other common programming languages, awards procedures full first-class status. This poses challenges for efficient implementation, but the resulting gain in expressive power is enormous.^[The 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 4.1, these variables are stored in the procedure’s environment.]
Exercise 1.40: Define a procedure cubic
that can be used together with the newtons-method
procedure in expressions of the form
(newtons-method (cubic a b c) 1)
to approximate zeros of the cubic
Exercise 1.41: Define a procedure double
that takes a procedure of one argument as argument and returns a procedure that applies the original procedure twice. For example, if inc
is 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 compose
that implements composition. For example, if inc
is a procedure that adds 1 to its argument,
((compose square inc) 6)
49
Exercise 1.43: If
((repeated square 2) 5)
625
Hint: You may find it convenient to use compose
from Exercise 1.42.
Exercise 1.44: The idea of smoothing a function is an important concept in signal processing. If smooth
that takes as input a procedure that computes smooth
and repeated
from Exercise 1.43.
Exercise 1.45: We saw in 1.3.3 that attempting to compute square roots by naively finding a fixed point of fixed-point
, average-damp
, and the repeated
procedure of Exercise 1.43. Assume that any arithmetic 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 computational strategy known as iterative improvement. Iterative 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 procedure iterative-improve
that takes two procedures as arguments: a method for telling whether a guess is good enough and a method for improving a guess. Iterative-improve
should return as its value a procedure that takes a guess as argument and keeps improving the guess until it is good enough. Rewrite the sqrt
procedure of 1.1.7 and the fixed-point
procedure of 1.3.3 in terms of iterative-improve
.