CSE131: Procedural Abstraction

Copyright © 1996-2005, Kenneth J. Goldman

Let's continue our discussion on computing the area of a circle. We saw that using a named constant PI and a radius variable r helped, since we don't have to continually write out the value of Pi.

However, we still had to keep writing the formula over and over. For example, we have to say r * r every time we want to square the radius, and then we have to remember to multiply by Pi.

Let's first address the problem of squaring a number. Instead of having to write the expression r * r every time, it would be nice to have a black box, or a procedure, that would take in a numerical argument and return the square of that argument.


If we had this, we could write

square(837)
instead of 837 * 837. This may not save us much typing but one can argue that the code is more readable, and certainly if we were taking cubes or higher powers, it would become ridiculous to start stringing the numbers all out in a line.

In Java, we can create a procedure called square as follows:

double square(double x) {
  return (x * x);
}

The "double" at the beginning is the type of return value from the procedure. This is followed by the name of the procedure, and then a list of formal parameters separated by commas. In this case, there is only one formal parameter, called x and its type is double.

When the procedure is invoked, the actual values passed in from the procedure call are bound to the formal parameters. The part in curly braces ({}) is the procedure body. The procedure body contains some number of statements that are executed in order. When one of the formal parameters is used inside the body, it's value is the value that was passed in from the calling procedure. The expression in the return statement is evaluated at the end of the execution of the procedure and the resulting value is returned to the calling procedure as the value of the procedure call.

Now let's invoke the procedure. Suppose we execute the line

double area3 = square(3);
The value 3 is called the actual parameter to which the formal parameter x will be bound. Java creates a little binding table for use inside the procedure and then evaluates the procedure body using that binding table. This little binding table is called an environment.

So, the actual parameter 3 is passed into the procedure square and bound to the variable x. Then the body of the procedure is executed. The return value 9 is computed, returned as the value of the expression "square(3)", and subsequently assigned to the variable area3.

Any expressions can be used in the actual parameter list when making a procedure call, provided that the types match the types of the corresponding formal parameter list. For example,

double x, y;
double r = 3;

x = square(2+1);    // 2 + 1 is evaluated, 3 is passed in
y = square(r);      // r is evaluated, 3 is passed in
i = square("3");    // ERROR: type String doesn't match double parameter

Example: areaOfCircle

Having a square procedure is fine, but what we really want is a procedure called areaOfCircle that compute the area of a circle, given the radius. Then we could write statements like
double area5 = areaOfCircle(5);

So, how do we write the procedure? Assuming that we have already defined the constant PI, we can write:

double areaOfCircle(double radius) {
  return (PI * square(radius));
}

Notice that we are using the square procedure to compute the square of the radius.

Let's do some more examples of procedures.

Example: hypotenuse

Suppose we are given a procedure sqrt that takes one parameter (a double) and returns its square root:

We don't know what sqrt is doing inside, but we can use it anyway because we understand its specification. This is a nice advantage of procedural abstraction.

Suppose we want a procedure hyp that finds the length of the hypotenuse of a right triangle, given the lengths of the other two sides.

Recalling that the length of the hypotenuse is the squre root of the sum of the squares of the lengths of the sides, we can write:

double hypotenuse(double sideA, double sideB) {
  return sqrt(square(sideA) + square(sideB));
}

And we can call the procedure as follows:

double hyp = hypotenuse(3,4);

Let's think about the evaluation step by step, using the substitution model:

hypotenuse(3,4)
sqrt(square(3) + square(4))
sqrt((3 * 3) + (4 * 4))
sqrt(9 + 16)
sqrt(25)
5

Built-in Mathematical Procedures

To get all of this to work, we need a procedure for square root. We could write one, but Java provides a lot of standard mathematical functions as methods of a static class called Math. (A method is a procedure defined as part of a class.)

We'll say more about classes later, but for now you can think of the math class as a collection of procedures (methods) and constants, accessed by Math.name-of-method or Math.name-of-constant. Examples include:

Math.sqrt(double x)    // returns the square root of x
Math.pow(x,y)          // returns x raised to the power y
Math.PI                // an approximation of pi
See the Math class documentation for a complete list of the available methods and constants.

Another useful class is the System class that provides an output stream for printing textual output from your program to the screen, as in the following example.

A Complete Program

Let's write a complete program using the procedures we have created. The program calculates hypotenuses of some right triangles.

public class Triangles {
   public static void main(String args[]) {
      test(3,4);
      test(9,12);
   }

   public static double square(double x) {
      return (x * x);
   }

   public static double hypotenuse(double sideA, double sideB) {
      return Math.sqrt(square(sideA) + square(sideB));
   }

   public static void test(double a, double b) {
      System.out.print("A right triangle with sides " + a + " and " + b);
      System.out.println(" has hypotenuse " + hypotenuse(a,b));
   }
}
Some notes:

So, the output of the program would be:

A right triangle with sides 3 and 4 has hypotenuse 5.
A right triangle with sides 9 and 12 has hypotenuse 15.

Advantages of Procedural Abstraction

To conclude, here are some advantages of procedural abstraction.

Reduction

Suppose we want to find the diagonal of a rectangle with sides a and b. The procedure could be written as
double rect_diag(double s) {
  return Math.sqrt(square(a) + square(b));
}
However, we can save ourselves some work by using what we've already done. We can reduce the problem of finding the length of the diagonal of a rectangle to the problem of finding the hypotenuse of a right triangle, as shown in the following diagram.


This would be coded as

double rect_diag(double a, double b) {
  return hypotenuse(a, b);
}
We've transformed one problem to another. This is called a reduction. This is useful in many areas of computer science.

Conditional statements

Let's do another example of procedural abstraction. Suppose that Math.abs wasn't built in. Thus, we want a black box like this:


The mathematical definition of this function is


double abs(double x) {
  return ???
Wait! How can we do something different depending on the value of x? So far, all of our computations have been uniform -- they do the same thing to every input, regardless of its value.

What we want is for the execution to be conditional on some test. For the test, we can use any boolean expression (also known as a predicate). But how do we make the execution conditional on whether the test is true or false?

For this purpose, Java provides conditional statements. One possible form is

if condition    // NO SEMICOLON!!
  consequent;
Here, the consequent is executed iff (if and only if) the condition is true. The other possible form is
if condition
  consequent1;
else
  consequent2;
Here, if the condition is true, then consequent1 is executed; otherwise, consequent2 is executed.

Using this construct, the abs example could be written as

double abs(double x) {
  if (x > 0)
    return x;
  else if (x == 0)
    return 0;
  else
    return -x;
}
We can shorten this a bit, but it's important to check that all possible cases are covered!
double abs(double x) {
  if (x >= 0)
    return x;
  else
    return -x;
}
Notice that execution continues after the conditional statement, so we could also have written
double abs(double x) {
  if (x >= 0)
    return x;
  return -x;
}
Note: indentation has no meaning in Java. It is only used for readability.

Using the return values from procedures as tests

The test in a conditional statement can be a simple expression or the result of a procedure, as long as it is a boolean expression. Consider


boolean inside(int x1, int y1, int x2, int y2, int px, int py) {
  return ((px >= x1) && (px <= x2) && (py >= y1) && (py <= y2));
}
Now we can use this procedure as a test in a conditional statement. For example,


if inside(3, 3, 10, 12, 5, 7) // rectangle: 3,3,10,12; point: 5,7
  System.out.println("The point is inside the rectangle.");
else
  System.out.println("The point is outside the rectangle.");
We can also use the results of procedures within boolean expressions. This is often useful in tests in conditional statements.
if (hypotenuse(3, 4) != 5)
  System.out.println("Error in hypotenuse test.");

Good and bad style in writing conditional statements