Now we'll look at a slightly more complex function, one which finds the roots of a quadratic equation. A Pascal version of such a function might be:
(* Solve quadtric equation, putting roots in root1 and root2
and returning the number of roots found. *)
function solveQuadratic(a, b, c: real; var root1, root2: real): int;
var
discriminant: real;
sqrtDiscriminant: real;
begin
discriminant := b * b - 4.0 * a * c;
if discriminant < 0.0 then
solveQuadratic := 0
else if discriminant = 0.0 then
begin
solveQuadratic := 1;
root1 := -b/(2.0 * a)
end
else
begin
solveQuadratic := 2;
sqrtDiscriminant := sqrt(discriminant);
root1 := (-b + sqrtDiscriminant)/(2.0 * a);
root2 := (-b - sqrtDiscriminant)/(2.0 * a)
end
end;
There are a few things to note here. The first is the interface to
the function: since we want to return up to two separate numbers, but
maybe zero or one in the degenerate cases, we can't just use the
normal paradigm of returning the result, because Pascal lets functions
accept many values, but return only one. To work around this
restriction, we return an integer indicating how many roots there
were, and use var parameters (also known as call by
reference parameters) to pass the actual results from the function
back to its caller. In order for this mechanism to work, the function
which calls solveQuadratic must define two variables for
receiving the results. The caller also has to be careful not to use
the variable root2 if there was only one root returned.
Other approaches, such as returning a record containing the roots and
the number of roots found, are also possible.
There are several other things worth mentioning. The local variable
sqrtDiscriminant is defined for the entire function, even
though it is only used in the body of the second else
clause. Because Pascal is strongly typed and does not allow
operations between integers and real numbers, we have to write all the
constants, even though they are integers, as real numbers in decimal
notation. Finally, we see that the body of the first if
clause does not have a begin and end because
it is a single statement.
var
parameters. Here's one approach:
// Solve quadratic equation, returning the roots
define method solve-quadratic(a, b, c) => (root1, root2);
let discriminant = b * b - 4 * a * c;
if (discriminant < 0)
values()
elseif (discriminant = 0)
values(- b / (2 * a))
else
let sqrt-discriminant = sqrt(discriminant);
values((- b + sqrt-discriminant) / (2 * a),
(- b - sqrt-discriminant) / (2 * a))
end if
end method solve-quadratic;
The first thing we notice is the notation for comments. Dylan has two
forms of comments. The sequence // introduces a comment
that continues to the end of the line; all the intervening text is
ignored by the language. The sequence /* introduces a
block comment that encloses all the text up to a */; the
enclosed text is ignored except that pairs of /* and
*/ inside the comment are matched, so block comments
nest. The nesting property of block comments means that it is safe to
use /* and */ to comment out code even if
that code contains block comments. (Dylan shares this comment
convention with C++, except that in C++, block comments don't nest.)
The most significant difference between this function and the Pascal version is what it returns. For the Dylan function, the comment says "returning the roots," and that is what it does. In Pascal, C, Fortran, and, in fact, most programming languages, a function can take an arbitrary number of arguments but the number of values returned can be only zero or one. (In Pascal, a function returning no values is called a procedure.) In Dylan, functions can return as many values as are appropriate for the task at hand, just as they can accept as many arguments as make sense.
How does a function return multiple values? We saw earlier that a function returns the value of the last expression that makes up its body. There is a built-in function named values which returns all of its arguments. By calling values as the last statement of a body, the arguments passed to values are used as the value of the body. For example, the following method could be used to return the sine and cosine of an angle:
define method sincos (angle) => (sin :: <real>, cos :: <real>);
values(sin(angle), cos(angle))
end method sincos;
To use both values returned returned by sincos, one uses
a special form of let which is said to accept multiple
values:
let (s, c) = sincos(theta); ...
Inside the body of the let, the variable s will hold the sine of theta
and c the cosine. Note, by the way, the return declaration for
sincos indicates that the method returns two real
numbers. The reason that names are used in return declarations is to
document which value is which in the case of multiple-value returning
functions.
As a special but typical case, when you are calling a function that returns more than one value and you are only interested in the first value, you don't have to bind the values with let, you can just use the function as an expression. In fact, there is no difference between calling a function that returns one value and calling a function that returns multiple values if you only want the first value returned.
Let's return to our quadratic equation solver. To see what the
function returns, we have to look for the last expression in the body.
In this case, the last statement is an if statement. In
Dylan, an if statement is an expression and can be used
anywhere a value is expected. The value of an if statement is the
value of the body of the branch that is taken. For example, in the
simple case of:
if (even?(n))
"even"
else
"odd"
end if
If the number n is even, the value of the if statement is
the string "even"; otherwise, the value is the string
"odd". Incidentally, Dylan is classified as an
"expression language" because all statements can return values and be
used as expressions.
In Pascal and C, one can construct a series of tests by putting if
statements in the else clause of another if
statemtent. In Dylan elseif is one word and not two, and
the elseif branch is actually part of the if statement.
There can be any number of elseif clauses between the
if clause and the optional else clause.
In solve-quadratic, the last statement of every branch of
the if statement is a call to values. If
the first test is true, values is called with no
arguments, so the function returns no values. If the test for the
elseif clause is true, the function returns one value,
the single root. (In this case, the call to values is unnecessary,
but it is useful as documentation to contrast with the cases where
zero and two values are returned.) Finally, if there are two roots,
both are returned.
How is this function used? In particular, how does a caller determine how many roots were found? If a caller is expecting more results than a function returns, all the variables for which no value was returned are given the false value. So, for example, we could write a function to count the number of roots as
define method number-of-quadratic-roots(a, b, c)
let (r1, r2) = solve-quadratic(a, b, c);
if (r2)
2
elseif (r1)
1
else
0
end if
end method number-of-quadratic-roots;
// Solve quadratic equation, returning the roots
define method solve-quadratic(a, b, c) => (root1, root2);
let discriminant = b * b - 4 * a * c;
case
negative?(discriminant) =>
values();
zero?(discriminant) =>
values(- b / (2 * a));
otherwise =>
let sqrt-discriminant = sqrt(discriminant);
values((- b + sqrt-discriminant) / (2 * a),
(- b - sqrt-discriminant) / (2 * a))
end case
end method solve-quadratic;
There are two differences here. First, the if statement
has been rewritten as a case statement. A
case statement contains a series of tests and bodies,
separated by the arrow symbol. The tests are checked in order and the
clause that corresponds to the first test which evaluates to true is
run, and the value of that clause is used as the result of the whole
statement. The last test in a case statement may be
otherwise: the otherwise clause is used if
none of the tests are true. Case statements are often
easier to read than long chains of elseifs, but choosing
one or the other is simply a matter of personal style.
Also, the tests have been changed from explicit comparisons with zero
to calls to function which do those tests. There is no real
difference between writing x < 0 and
negative?(x), but sometimes one form is clearer than the
other. A function which returns a boolean value is called a
predicate in Dylan, and, by convention, the names of predicates
end in question marks, as we can see from negative? and
zero?.
Copyright © 1995 Paul Haahr. All rights reserved.
Paul Haahr