Section 2.1
Introduction to Data Abstraction

In §1.1.8, we noted of how the procedure was implemented could be suppressed, and the particular procedure itself could be replaced by any other procedure with the same overall behavior. In other words, we could make an abstraction that would separate the way the procedure would be used from the details of how the procedure would be implemented in terms of more primitive procedures. The analogous notion for compound data is called data abstraction. Data abstraction is a methodology that enables us to isolate how a compound data object is used from the details of how it is constructed from more primitive data objects.

The basic idea of data abstraction is to structure the programs that are to use compound data objects so that they operate on abstract data. That is, our programs should use data in such a way as to make no assumptions about the data that are not strictly necessary for performing the task at hand. At the same time, a concrete data representation is defined independent of the programs that use the data. The interface between these 2 parts of our system will be a set of procedures, called selectors and constructors, that implement the abstract data in terms of the concrete representation. To illustrate this technique, we will consider how to design a set of procedures for manipulating rational numbers.

Example: Arithmetic Operations for Rational Numbers

Suppose we want to do arithmetic with rational numbers. We want to be able to add, subtract, multiply, and divide them and to test whether 2 rational numbers are equal.

Let us begin by assuming that we already have a way of constructing a rational number from a numerator and a denominator. We also assume that, given a rational number, we have a way of extracting (or selecting) its numerator and its denominator. Let us further assume that the constructor and selectors are available as procedures:

We are using here a powerful strategy of synthesis: wishful thinking. We haven't yet said how a rational number is represented, or how the procedures numer, denom, and make-rat should be implemented. Even so, if we did have these 3 procedures, we could then add, subtract, multiply, divide, and test equality by using the following relations: \begin{align} \frac{n_1}{d_1} = \frac{n_2}{d_2}&\ i\!f\!\!f\ n_1 d_2 = n_2 d_1 , \\ \frac{n_1}{d_1} + \frac{n_2}{d_2}&=\frac{n_1 d_2 + n_2 d_1 }{d_1 d_2}, \\ \frac{n_1}{d_1} - \frac{n_2}{d_2}&=\frac{n_1 d_2 - n_2 d_1 }{d_1 d_2}, \\ \frac{n_1}{d_1}\times\frac{n_2}{d_2}&=\frac{n_1 n_2\qquad\quad}{d_1 d_2}, \\ \frac{n_1}{d_1} \div \frac{n_2}{d_2}&=\frac{n_1 d_2\qquad\quad}{d_1 n_2}. \end{align} We can express these rules as procedures:

   (define (equal-rat? x y)
     (= (* (numer x) (denom y))
        (* (numer y) (denom x))))
   (define (add-rat x y)
     (make-rat (+ (* (numer x) (denom y))
                  (* (numer y) (denom x)))
               (* (denom x) (denom y))))
   (define (sub-rat x y)
     (make-rat (- (* (numer x) (denom y))
                  (* (numer y) (denom x)))
               (* (denom x) (denom y))))
   (define (mul-rat x y)
     (make-rat (* (numer x) (numer y))
               (* (denom x) (denom y))))
   (define (div-rat x y)
     (make-rat (* (numer x) (denom y))
               (* (denom x) (numer y))))
Now we have the operations on rational numbers defined in terms of the selector and constructor procedures numer, denom, and make-rat. But we haven't yet defined these. What we need is some way to glue together a numerator and a denominator to form a rational number.

Pairs

To enable us to implement the concrete level of our data abstraction, our language provides a compound structure called a pair, which can be constructed with the primitive procedure cons It stands for construct.. This procedure takes 2 arguments and returns a compound data object that contains the 2 arguments as parts. Given a pair, we can extract the parts using the primitive procedures car and cdr They derive from the original implementation of Lisp on the machine IBM 704, which had an addressing scheme allowing reference to the address and decrement parts of a memory location:
  car stands for contents of address part of register, and
  cdr stands for contents of decrement part of register, pronounced could-er.
. Thus, we can use cons, car, and cdr as follows:

   (define x (cons 1 2))

   (car x)
   1

   (cdr x)
   2
Notice that a pair is a data object that can be given a name and manipulated, just like a primitive data object. Moreover, cons can be used to form pairs whose elements are pairs, and so on:
   (define x (cons 1 2))

   (define y (cons 3 4))

   (define z (cons x y))

   (car (car z))
   1

   (car (cdr z))
   3
In §2.2 we will see how this ability to combine pairs means that pairs can be used as general-purpose building blocks to create all sorts of complex data structures. The single compound-data primitive pair, implemented by the procedures cons, car, and cdr, is the only glue we need. Data objects constructed from pairs are called list-structured data.

Representing rational numbers

Pairs offer a natural way to complete the rational-number system. Simply represent a rational number as a pair of 2 integers: a numerator and a denominator. Then make-rat, numer, and denom Another way to define the selectors and constructor is
   (define make-rat cons)
   (define numer car)
   (define denom cdr)

The 1st definition associates the name make-rat with the value of the expression cons, which is the primitive procedure that constructs pairs. Thus make-rat and cons are names for the same primitive constructor. Defining selectors and constructors in this way is efficient:
Instead of make-rat calling cons, make-rat is cons, so there is only one procedure called, not 2, when make-rat is called. On the other hand, doing this defeats debugging aids that trace procedure calls or put breakpoints on procedure calls: You may want to watch make-rat being called, but you certainly don't want to watch every call to cons. We have chosen not to use this style of definition in this book.
are readily implemented as follows:

   (define (make-rat n d) (cons n d))

   (define (numer x) (car x))

   (define (denom x) (cdr x))
Also, in order to display the results of our computations, we can print Display is the Scheme primitive for printing data. The Scheme primitive newline starts a new line for printing. Neither of these procedures returns a useful value, so in the uses of print-rat below, we show only what print-rat prints, not what the interpreter prints as the value returned by print-rat. rational numbers by printing the numerator, a slash, and the denominator:
   (define (print-rat x)
     (newline)
     (display (numer x))
     (display "/")
     (display (denom x)))
Now we can try our rational-number procedures:
   (define one-half (make-rat 1 2))

   (print-rat one-half)
   1/2

   (define one-3rd (make-rat 1 3))
   (print-rat (add-rat one-half one-3rd))
   5/6

   (print-rat (mul-rat one-half one-3rd))
   1/6

   (print-rat (add-rat one-3rd one-3rd))
   6/9
As the final example shows, our rational-number implementation does not reduce rational numbers to lowest terms. We can remedy this by changing make-rat. If we have a gcd procedure like the one in §1.2.5 that produces the greatest common divisor of 2 integers, we can use gcd to reduce the numerator and the denominator to lowest terms before constructing the pair:
   (define (make-rat n d)
     (let ((g (gcd n d)))
       (cons (/ n g) (/ d g))))
Now we have
   (print-rat (add-rat one-3rd one-3rd))
   2/3
as desired. This modification was accomplished by changing the constructor make-rat without changing any of the procedures (such as add-rat and mul-rat) that implement the actual operations.

Exercise

Abstraction Barriers

Before continuing with more examples of compound data and data abstraction, let us consider some of the issues raised by the rational-number example. We defined the rational-number operations in terms of a constructor make-rat and selectors numer and denom. In general, the underlying idea of data abstraction is to identify for each type of data object a basic set of operations in terms of which all manipulations of data objects of that type will be expressed, and then to use only those operations in manipulating the data.

Fig 2.1: Data-abstraction barriers
Fig 2.1: Data-abstraction barriers in the rational-
number package.

We can envision the structure of the rational-number system as shown in Fig 2.1. The horizontal lines represent abstraction barriers that isolate different levels of the system. At each level, the barrier separates the programs (above) that use the data abstraction from the programs (below) that implement the data abstraction. Programs that use rational numbers manipulate them solely in terms of the procedures supplied for public use by the rational-number package: add-rat, sub-rat, mul-rat, div-rat, and equal-rat?. These, in turn, are implemented solely in terms of the constructor and selectors make-rat, numer, and denom, which themselves are implemented in terms of pairs. The details of how pairs are implemented are irrelevant to the rest of the rational-number package so long as pairs can be manipulated by the use of cons, car, and cdr. In effect, procedures at each level are the interfaces that define the abstraction barriers and connect the different levels.

This simple idea has many advantages. One advantage is that it makes programs much easier to maintain and to modify. Any complex data structure can be represented in a variety of ways with the primitive data structures provided by a programming language. Of course, the choice of representation influences the programs that operate on it; thus, if the representation were to be changed at some later time, all such programs might have to be modified accordingly. This task could be time-consuming and expensive in the case of large programs unless the dependence on the representation were to be confined by design to a very few program modules.

For example, an alternate way to address the problem of reducing rational numbers to lowest terms is to perform the reduction whenever we access the parts of a rational number, rather than when we construct it. This leads to different constructor and selector procedures:

   (define (make-rat n d)
     (cons n d))
   (define (numer x)
     (let ((g (gcd (car x) (cdr x))))
       (/ (car x) g)))
   (define (denom x)
     (let ((g (gcd (car x) (cdr x))))
       (/ (cdr x) g)))
The difference between this implementation and the previous one lies in when we compute the gcd. If in our typical use of rational numbers we access the numerators and denominators of the same rational numbers many times, it would be preferable to compute the gcd when the rational numbers are constructed. If not, we may be better off waiting until access time to compute the gcd. In any case, when we change from one representation to the other, the procedures add-rat, sub-rat, and so on do not have to be modified at all.

Constraining the dependence on the representation to a few interface procedures helps us design programs as well as modify them, because it allows us to maintain the flexibility to consider alternate implementations. To continue with our simple example, suppose we are designing a rational-number package and we can't decide initially whether to perform the gcd at construction time or at selection time. The data-abstraction methodology gives us a way to defer that decision without losing the ability to make progress on the rest of the system.

Exercise

What Is Meant by Data?

We began the rational-number implementation in §2.1.1 by implementing the rational-number operations add-rat, sub-rat, and so on in terms of 3 unspecified procedures: make-rat, numer, and denom. At that point, we could think of the operations as being defined in terms of data objects -- numerators, denominators, and rational numbers -- whose behavior was specified by the latter 3 procedures.

But exactly what is meant by data? It is not enough to say whatever is implemented by the given selectors and constructors. Clearly, not every arbitrary set of 3 procedures can serve as an appropriate basis for the rational-number implementation. We need to guarantee that, if we construct a rational number x from a pair of integers n and d, then extracting the numer and the denom of x and dividing them should yield the same result as dividing n by d. In other words, make-rat, numer, and denom must satisfy the condition that, for any integer n and any non-zero integer d, if x is (make-rat n d), then $$ {{\tt (numer\ x)} \over {\tt (denom\ x)}} = {{\tt n} \over {\tt d}} $$ In fact, this is the only condition make-rat, numer, and denom must fulfill in order to form a suitable basis for a rational-number representation. In general, we can think of data as defined Surprisingly, this idea is very difficult to formulate rigorously. There are 2 approaches to giving such a formulation:
One, pioneered by C. A. R. Hoare (1972), is known as the method of abstract models. It formalizes the procedures plus conditions specification as outlined in the rational-number example above. Note that the condition on the rational-number representation was stated in terms of facts about integers (equality and division). In general, abstract models define new kinds of data objects in terms of previously defined types of data objects. Assertions about data objects can therefore be checked by reducing them to assertions about previously defined data objects.
Another approach, introduced by Zilles at MIT, by Goguen, Thatcher, Wagner, and Wright at IBM (see Thatcher, Wagner, and Wright 1978), and by Guttag at Toronto (see Guttag 1977), is called algebraic specification. It regards the procedures as elements of an abstract algebraic system whose behavior is specified by axioms that correspond to our conditions, and uses the techniques of abstract algebra to check assertions about data objects. Both methods are surveyed in the paper by Liskov and Zilles (1975).
by some collection of selectors and constructors, together with specified conditions that these procedures must fulfill in order to be a valid representation.

This point of view can serve to define not only high-level data objects, such as rational numbers, but lower-level objects as well. Consider the notion of a pair, which we used in order to define our rational numbers. We never actually said what a pair was, only that the language supplied procedures cons, car, and cdr for operating on pairs. But the only thing we need to know about these 3 operations is that if we glue 2 objects together using cons we can retrieve the objects using car and cdr. That is, the operations satisfy the condition that, for any objects x and y, if z is (cons x y) then (car z) is x and (cdr z) is y. Indeed, we mentioned that these 3 procedures are included as primitives in our language. However, any triple of procedures that satisfies the above condition can be used as the basis for implementing pairs. This point is illustrated strikingly by the fact that we could implement cons, car, and cdr without using any data structures at all but only using procedures. Here are the definitions:

   (define (cons x y)
     (define (dispatch m)
       (cond ((= m 0) x)
             ((= m 1) y)
             (else (error "Argument not 0 or 1 -- CONS" m))))
     dispatch)

   (define (car z) (z 0))

   (define (cdr z) (z 1))
This use of procedures corresponds to nothing like our intuitive notion of what data should be. Nevertheless, all we need to do to show that this is a valid way to represent pairs is to verify that these procedures satisfy the condition given above.

The subtle point to notice is that the value returned by (cons x y) is a procedure -- namely the internally defined procedure dispatch, which takes one argument and returns either x or y depending on whether the argument is 0 or 1. Correspondingly, (car z) is defined to apply z to 0. Hence, if z is the procedure formed by (cons x y), then z applied to 0 will yield x. Thus, we have shown that (car (cons x y)) yields x, as desired. Similarly, (cdr (cons x y)) applies the procedure returned by (cons x y) to 1, which returns y. Therefore, this procedural implementation of pairs is a valid implementation, and if we access pairs using only cons, car, and cdr we cannot distinguish this implementation from one that uses real data structures.

The point of exhibiting the procedural representation of pairs is not that our language works this way (Scheme, and Lisp systems in general, implement pairs directly, for efficiency reasons) but that it could work this way. The procedural representation, although obscure, is a perfectly adequate way to represent pairs, since it fulfills the only conditions that pairs need to fulfill. This example also demonstrates that the ability to manipulate procedures as objects automatically provides the ability to represent compound data. This may seem a curiosity now, but procedural representations of data will play a central role in our programming repertoire. This style of programming is often called message passing, and we will be using it as a basic tool in chapter 3 when we address the issues of modeling and simulation.

Exercise

Extended Exercise: Interval Arithmetic

Alyssa P. Hacker is designing a system to help people solve engineering problems. One feature she wants to provide in her system is the ability to manipulate inexact quantities (such as measured parameters of physical devices) with known precision, so that when computations are done with such approximate quantities the results will be numbers of known precision.

Electrical engineers will be using Alyssa's system to compute electrical quantities. It is sometimes necessary for them to compute the value of a parallel equivalent resistance $R_p$ of 2 resistors $R_1$ and $R_2$ using the formula $$ R_p = {1 \over 1/R_1 + 1/R_2} $$ Resistance values are usually known only up to some tolerance guaranteed by the manufacturer of the resistor. For example, if you buy a resistor labeled 6.8 ohms with 10% tolerance you can only be sure that the resistor has a resistance between 6.8 - 0.68 = 6.12 and 6.8 + 0.68 = 7.48 ohms. Thus, if you have a 6.8-ohm 10% resistor in parallel with a 4.7-ohm 5% resistor, the resistance of the combination can range from about 2.58 ohms (if the 2 resistors are at the lower bounds) to about 2.97 ohms (if the 2 resistors are at the upper bounds).

Alyssa's idea is to implement interval arithmetic as a set of arithmetic operations for combining intervals (objects that represent the range of possible values of an inexact quantity). The result of adding, subtracting, multiplying, or dividing 2 intervals is itself an interval, representing the range of the result.

Alyssa postulates the existence of an abstract object called an interval that has 2 endpoints: a lower bound and an upper bound. She also presumes that, given the endpoints of an interval, she can construct the interval using the data constructor make-interval. Alyssa 1st writes a procedure for adding 2 intervals. She reasons that the minimum value the sum could be is the sum of the 2 lower bounds and the maximum value it could be is the sum of the 2 upper bounds:

   (define (add-interval x y)
     (make-interval (+ (lower-bound x) (lower-bound y))
                    (+ (upper-bound x) (upper-bound y))))
Alyssa also works out the product of 2 intervals by finding the minimum and the maximum of the products of the bounds and using them as the bounds of the resulting interval. (Min and max are primitives that find the minimum or maximum of any number of arguments.)
   (define (mul-interval x y)
     (let ((p1 (* (lower-bound x) (lower-bound y)))
           (p2 (* (lower-bound x) (upper-bound y)))
           (p3 (* (upper-bound x) (lower-bound y)))
           (p4 (* (upper-bound x) (upper-bound y))))
       (make-interval (min p1 p2 p3 p4)
                      (max p1 p2 p3 p4))))
To divide 2 intervals, Alyssa multiplies the 1st by the reciprocal of the 2nd. Note that the bounds of the reciprocal interval are the reciprocal of the upper bound and the reciprocal of the lower bound, in that order.
   (define (div-interval x y)
     (mul-interval x 
                   (make-interval (/ 1.0 (upper-bound y))
                                  (/ 1.0 (lower-bound y)))))

Exercise