Functions,
as introduced above, are much like ordinary mathematical functions. They
specify a value that is determined by one or more parameters. But there
is an important difference between mathematical functions and computer
functions.
Computer functions
must be effective.
As a case in point, consider the problem of computing square
roots. We can define the square-root function as
具体例として、平方根を計算する問題を考えてみましょう。平方根関数は次のように定義できます。
\[
\sqrt{x}\ =\text{ the }y\text{ such that }y \geq 0\text{ and }
y^2\ =\ x
\]
This describes a perfectly legitimate mathematical function. We could
use it to recognize whether one number is the square root of another, or
to derive facts about square roots in general. On the other hand, the
definition does not describe a
computer function.
Indeed, it tells us almost nothing about how to actually find the square
root of a given number. It will not help matters to rephrase this
definition in
pseudo-JavaScript:
function sqrt(x) {
return the y $\texttt{with}$ y >= 0 && square(y) === x;
}
This only begs the question.
これでは問題を先送りしているだけです。
The contrast between
mathematical function and computer function
is a reflection of the general distinction between describing properties of
things and describing how to do things, or, as it is sometimes referred to,
the distinction between
declarative knowledge and imperative knowledge. In
mathematics we are usually concerned with declarative (what is)
descriptions, whereas in computer science we are usually concerned
with imperative (how to) descriptions.
How does one compute
square roots? The most common way is to use
Newton's method of successive approximations, which says that whenever
we have a guess $y$ for the value of the square
root of a number $x$, we can perform a simple
manipulation to get a better guess (one closer to the actual square root)
by averaging $y$ with
$x/y$.
Continuing this process, we obtain better and better approximations to the
square root.
この処理を続けると、平方根へのより良い近似値が次々と得られます。
Now let's formalize the process in terms of functions. We start with
a value for the
radicand (the number whose square root we are trying to compute) and a value
for the guess. If the guess is good enough for our purposes, we are done;
if not, we must repeat the process with an improved guess. We write this
basic strategy as a
function:
A guess is improved by averaging it with the quotient of the radicand and
the old guess:
推測値は、被開平数を古い推測値で割った商との平均を取ることで改善されます。
function improve(guess, x) {
return average(guess, x / guess);
}
where
ここで
function average(x, y) {
return (x + y) / 2;
}
We also have to say what we mean by good enough. The
following will do for illustration, but it is not really a very good
test. (See exercise 1.7.)
The idea is to improve the answer until it is close enough so that its
square differs from the radicand by less than a predetermined
tolerance (here 0.001):
The sqrt program also illustrates that the
simple
functional
language we have introduced so far is sufficient for writing any purely
numerical program that one could write in, say, C or Pascal. This might
seem surprising, since we have not included in our language any iterative
(looping) constructs that direct the computer to do something over and over
again.
The function sqrt_iter,
on the other hand, demonstrates how iteration can be accomplished using no
special construct other than the ordinary ability to call a
function.
Alyssa P. Hacker doesn't like the syntax of
conditional
expressions, involving the characters ?
and :.
Why can't I just
declare an ordinary conditional function whose application
works just like conditional expressions?
she asks.
Alyssa's friend Eva Lu Ator claims this can indeed be
done, and she declares a conditional
function as follows:
Alyssa P. Hacker は、
? と : を使う条件式の構文が気に入りません。「条件式と同じように動作する通常の条件関数を宣言するだけではだめなの?」と彼女は尋ねます。Alyssa の友人 Eva Lu Ator は確かにそれは可能だと言い、次のように conditional 関数を宣言します。
What happens when Alyssa attempts to use this to compute square roots?
Explain.
Alyssa がこれを使って平方根を計算しようとすると何が起こるでしょうか?説明してください。
Any call of sqrt_iter
leads immediately to an infinite loop. The reason for this is our
applicative-order evaluation. The evaluation of the return expression
of sqrt_iter needs to evaluate
its arguments first, including the recursive call of
sqrt_iter, regardless whether the
predicate evaluates to true or false. The same of
course happens with the recursive call, and thus the function
conditional never actually gets
applied.
The
is_good_enough
test used in computing square roots will not be very effective for finding
the square roots of very small numbers. Also, in real computers, arithmetic
operations are almost always performed with limited precision. This makes
our test inadequate for very large numbers. Explain these statements, with
examples showing how the test fails for small and large numbers. An
alternative strategy for implementing
is_good_enough
is to watch how guess changes from one
iteration to the next and to stop when the change is a very small fraction
of the guess. Design a square-root
function
that uses this kind of end test. Does this work better for small and
large numbers?
The absolute tolerance of 0.001 is too large when computing the square
root of a small value. For example,
sqrt(0.0001)
results in 0.03230844833048122 instead of the expected value 0.01,
an error of over 200%.
On the other hand, for very large values, rounding errors might make
the algorithm fail to ever get close enough to the square root, in which
case it will not terminate.
The following program alleviates the problem by replacing an absolute
tolerance with a relative tolerance.
Newton's method for
cube roots is based on the fact that if
$y$ is an
approximation to the cube root of $x$, then a better approximation is
given by the value
Use this formula to implement a cube-root
function
analogous to the square-root
function.
(In section 1.3.4 we will see how to
implement Newton's method in general as an abstraction of these
square-root and cube-root
functions.)
Declarative and
imperative descriptions are intimately related, as indeed are
mathematics and computer science. For instance, to say that the
answer produced by a program is
correct is to make a declarative statement about the program.
There is a large amount of research aimed at establishing techniques for
proving that programs are correct, and much of the technical difficulty of
this subject has to do with negotiating the transition between imperative
statements (from which programs are constructed) and declarative statements
(which can be used to deduce things).
In a related vein, programming language designers have explored
so-called
very high-level languages, in which one actually programs in terms of
declarative statements.
The idea is to make interpreters sophisticated
enough so that, given what is knowledge specified by the
programmer, they can generate how to knowledge automatically.
This cannot be done in general, but there are important areas where progress
has been made. We shall revisit this idea in chapter 4.
This square-root algorithm is
actually a special case of Newton's method, which is a general
technique for finding roots of equations. The square-root algorithm itself
was developed by Heron of
Alexandria in the first century CE. We will see how to express
the general Newton's method as a
JavaScript function
in section 1.3.4.
Readers who are worried about the efficiency issues involved in using
function
calls to implement iteration should note the remarks on tail
recursion in
section 1.2.1.