;;; Implementing Rational Numbers and Generic Arithmetic (Part 1) ;;; Summary of lecture from Programming Languages morning section ;;; Sept 26, 2001 ;;; Stewart M. Clamen ;;; USAGE NOTE: This file is formatted to be loadable into a running ;;; Scheme session. As arguments are made incrementally, though, you ;;; might want to pass the definitions into the Scheme interpreter one ;;; at a time. ;; This implementation of Scheme (scheme48) already has rational ;; numbers as a built-in data-type, but let us pretend for today that ;; it doesn't, and implement rational numbers on terms of more ;; primitive Scheme mechanisms. ;; Here is a simple implementation of Rationals. MAKE-RAT, NUMER, and ;; DENOM constitute the "interface" to our implementation, being the ;; only procedures that need to know how our rationals are ;; represented. (define (make-rat n d) (cons n d)) (define (numer rat) (car rat)) (define (denom rat) (cdr rat)) ;; Rationals are represented here as a pair of integers. We have one ;; "constructor" procedures, one official way to make a rational: ;; MAKE-RAT, and two "accessor" procedures, NUMER and DENOM. ;; If we type (make-rat 1 2) into the scheme48 interpreter, we get ;; back '(1 . 2). This is the interpreter displaying the concrete ;; representation of an object. Let's create a print procedures for ;; our rationals. (define (print-rat rat) (display (numer rat)) (display "/") (display (denom rat)) (newline)) ;; There's no way to get the interpreter to call our print procedure ;; automatically, so we have to call it explictly, to wit: ;; We type: (print-rat (make-rat 1 2)) ;; The interpreter replies: 1/2 ;; Notice what happens if we type (print-rat (make-rat 2 4)) ;; The interpreter replies: 2/4 ;; Rational numbers are usually expressed in a canonical, reduced ;; form, there the numerator and denominator are relatively prime. We ;; can ensure that in our representation by replacing the make-rat ;; procedure with something a little more complicated. (define (make-rat n d) (let ((g (gcd n d))) (cons (/ n g) (/ d g)))) ;; GCD is a built-in scheme procedure for calculating the greatest ;; common divisor of two numbers. ;; Our representation of rationals hasn't changed, so there is no need ;; to change any of the other procedures we've defined to date. ;; Now let's define the basic arithmetic operations in terms of this ;; representation: ;; Multiplication (define (rat* rat1 rat2) (make-rat (* (numer rat1) (numer rat2)) (* (denom rat1) (denom rat2)))) ;; Division (define (rat/ rat1 rat2) (make-rat (* (numer rat1) (denom rat2)) (* (denom rat1) (numer rat2)))) ;; Addition (define (rat+ rat1 rat2) (make-rat (+ (* (numer rat1) (denom rat2)) (* (numer rat2) (denom rat1))) (* (denom rat1) (denom rat2)))) ;; Subtraction (define (rat- rat1 rat2) (make-rat (- (* (numer rat1) (denom rat2)) (* (numer rat2) (denom rat1))) (* (denom rat1) (denom rat2)))) ;; Equality (define (rat= rat1 rat2) (and ((= (numer rat1) (numer rat2)) (= (denom rat1) (denom rat2))))) ;; Here are some more programs written against our Rational Number interface (define (integer->rat i) (make-rat i 1)) (define (reciprocal-rat rat) (rat/ (integer->rat 1) rat)) (define (inverse-rat rat) (rat- (integer->rat 0) rat)) (define (square-rat rat) (rat* rat rat)) ;; Notice that all the procedures introduced after MAKE-RAT, NUMER, ;; and DENOM make no reference to the representation of our ;; rationals. They respect the "abstract interface" introduced by ;; those three procedures. This gives us great flexibility. We could ;; change the representation of our rationals just be redefining the ;; implementation of the interface. Here is such an alternative ;; representation: (define (make-rat n d) (let ((g (gcd n d))) (vector (/ n g) (/ d g)))) (define (numer rat) (vector-ref rat 0)) (define (denom rat) (vector-ref rat 1)) ;; If you have been following this lecture by entering the definitions ;; in a running Scheme, you can perform the following experiment: ;; Load everything up until this new implementation for rationals is ;; introduced into the interpreter, and evaluate these expressions: ;; (print-rat (make-rat 1 2)) ;; returns: 1/2 ;; (print-rat (rat+ (make-rat 1 2) (make-rat 1 3))) ;; returns: 5/6 ;; (print-rat (rat* (make-rat 1 2) (make-rat 1 3))) ;; returns: 1/6 ;; Now, change the implementation of rationals by entering the three ;; new definitions above, then evaluate the three "print-rat" ;; expressions again. ;; Before we continue, let's return (for simplicity) to our original ;; implementation (define (make-rat n d) (let ((g (gcd n d))) (cons (/ n g) (/ d g)))) (define (numer rat) (car rat)) (define (denom rat) (cdr rat)) ;; The arithmetic procedures defined above only work on our rationals. ;; We know how to convert regular Scheme integers into our rationals ;; (see INTEGER->RATIONAL above), so let us set up arithmetic ;; procedures that can accept either our rationals or integers as ;; arguments. ;; Let's work on a direct implementation of GEN+, a generic addition ;; procedure. ;; First, we need to correct an oversight of our rational-numbers ;; design. We need a type-predicate, a procedure (let's call it ;; RATIONAL?) to verifies that its argument is one of our rationals? ;; Here's a reasonably good one (define (rat? rat) (and (pair? rat) (integer? (car rat)) (integer? (cdr rat)) (not (= (cdr rat) 0)) )) ;; Because RAT? has to know the details of our rational ;; implementation, it joins MAKE-RAT, NUMER, and DENOM as part of our ;; abstract interface. ;; Let's note in passing that none of the procedure we've defined to ;; date bother to type-check their arguments, to verify that the ;; arguments really are our rationals when they need to be. ;; We can introduce type-checking in all of them just by making NUMER ;; and DENOM type-check. This is because all of the arithmetic ;; procedures we defined were written against them, the public ;; interface. (define (numer rat) (if (rat? rat) (car rat) "ERROR: not a rational")) (define (denom rat) (if (rat? rat) (cdr rat) "ERROR: not a rational")) ;; As an experiment, introduce these definitions into a running ;; Scheme session, and see how the previously-defined RAT+ now reports ;; a useful error if you try to evaluate an expression like: (RAT+ 4 4) ;; OK, now we can write GEN+, our (more) generic addition procedure. (define (gen+ n1 n2) (cond ((and (rat? n1) (integer? n2)) (rat+ n1 (integer->rat n2))) ((and (integer? n1) (rat? n2)) (rat+ (integer->rat n1) n2)) ((and (integer? n1) (integer? n2)) (rat+ (integer->rat n1) (integer->rat n2))) (else ;; both are rationals (rat+ n1 n2)))) ;; That's a little verbose. We can use the LET construct to simplify ;; the logic. (define (gen+ n1 n2) (let ((r1 (if (rat? n1) n1 (integer->rat n1))) (r2 (if (rat? n2) n2 (integer->rat n2)))) (rat+ r1 r2))) ;; There, that's cleaner. Let's do GEN- now: (define (gen- n1 n2) (let ((r1 (if (rat? n1) n1 (integer->rat n1))) (r2 (if (rat? n2) n2 (integer->rat n2)))) (rat- r1 r2))) ;; Notice how similar the two routines are. GEN* and GEN/ would look ;; the same as well. Rather than define them directly, this gives us ;; an opportunity to introduce a higher-order procedure to capture the ;; commonality. (define (generic-operator op) (lambda (n1 n2) (let ((r1 (if (rat? n1) n1 (integer->rat n1))) (r2 (if (rat? n2) n2 (integer->rat n2)))) (op r1 r2)))) ;; GENERIC-OPERATOR takes a rational binary procedure as an argument, ;; and returns a generic binary procedure. Using it, we can define ;; generic multiplication and division like so: (define gen* (generic-operator rat*)) (define gen/ (generic-operator rat/))