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 two 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 three procedures, we could then add, subtract, multiply, divide, and test equality by using the following relations:
We can express these rules as procedures:
(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))))
(define (equal-rat? x y)
(= (* (numer x) (denom y))
(* (numer y) (denom x))))
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. This procedure takes two arguments and returns a compound data
object that contains the two arguments as parts. Given a pair, we can
extract the parts using the primitive procedures
car and
cdr.
Thus, we can use cons,
car, and cdr as follows:
(define x (cons 1 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:(car x) 1
(cdr x) 2
(define x (cons 1 2))In section(define y (cons 3 4))
(define z (cons x y))
(car (car z)) 1
(car (cdr z)) 3
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 two integers: a
numerator and a denominator. Then make-rat, numer, and
denom are readily implemented as follows:
(define (make-rat n d) (cons n d))Also, in order to display the results of our computations, we can print rational numbers by printing the numerator, a slash, and the denominator:(define (numer x) (car x))
(define (denom x) (cdr x))
(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-third (make-rat 1 3)) (print-rat (add-rat one-half one-third)) 5/6
(print-rat (mul-rat one-half one-third)) 1/6
(print-rat (add-rat one-third one-third)) 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 section
that produces the greatest common divisor of two
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-third one-third)) 2/3as desired. This modification was accomplished by changing the constructor make-rat without changing any of the procedures (such as addrat and mul-rat) that implement the actual operations.
Exercise. Define a better version of make-rat that handles both positive and negative arguments. Make-rat should normalize the sign so that if the rational number is positive, both the numerator and denominator are positive, and if the rational number is negative, only the numerator is negative.