sicp-ex-2.79



<< Previous exercise (2.78) | Index | Next exercise (2.80) >>


 ;; ----------------------------------------------- 
 ;; EXERCISE 2.79 
 ;; ----------------------------------------------- 
  
 (define (install-scheme-number-package) 
   ;; ... 
   (put 'equ? '(scheme-number scheme-number) =) 
   'done) 
  
 (define (install-rational-package) 
   ;; ... 
   (define (equ? x y) 
     (= (* (numer x) (denom y)) (* (numer y) (denom x)))) 
   ;; ... 
   (put 'equ? '(rational rational) equ?) 
   'done) 
  
 (define (install-complex-package) 
   ;; ... 
   (define (equ? x y) 
     (and (= (real-part x) (real-part y)) (= (imag-part x) (imag-part y)))) 
   ;; ... 
   (put 'equ? '(complex complex) equ?) 
   'done) 
  
 (define (equ? x y) (apply-generic 'equ? x y)) 

I think it's best to define equ? in each implementation of complex:

 (define (install-rectangular-package) 
   ;; ... 
   (put 'equ? '(rectangular rectangular) 
        (lambda (x y) (and (= (real-part x) (real-part y)) 
                           (= (imag-part x) (imag-part y))))) 
   'done) 
  
 (define (install-polar-package) 
   ;; ... 
   (put 'equ? '(polar polar) 
        (lambda (x y) (and (= (magnitude x) (magnitude y)) 
                           (= (angle x) (angle y))))) 
   'done) 
  
 (define (equ? x y) (apply-generic 'equ? x y)) 
  
 (define (install-complex-packages) 
   ;; ... 
   (put 'equ? '(complex complex) equ?) 
   'done) 

The above solution for defining equality for polar terms is incorrect. Two polar numbers are equal when they have the same magnitude and have angles that are congruent modulo 2π (where the angles are measured in radians). Additionally, if the magnitude of a polar form is 0, it is equal to all other polar values with magnitude 0, regardless of their angle (i.e. r=0, theta=10 is equivalent to r=0, theta=0).


Another issue with this second solution is now you need to consider 4 cases:

(1) `(rectangular rectangular)

(2) `(polar rectangular)

(3) `(rectangular polar)

(4) `(polar polar)

In my opinion, leaving equ? up to the complex number package is a far better abstraction.


All these solutions are bad, because they require the modification of the original packages, and thus break additivity.

Liskov

My solution does not require to modifying the other packages. It does not work with mixed types, because I have used the generic operation 'sub'. That is fine, because the enunciate of the exercise do not specify that the 'equ?' operation should work with mixed types. But if you want, the code below can work with coercion procedures (like 'rational->complex'), or with 'apply-generic' of ex-2.82.

  
 (define (equ? x y) 
   ((get 'equ? 'generic) x y)) 
  
 (define (install-generic-arithmetic-package) 
   ;; tolerance is Ɛ * 4. If it was zero, the last test (that with the PI constant) would fail. 
   (define tolerance 
     (* ((lambda () 
           (define (try value) 
             (let ((next-value (/ value 2))) 
               (if (= (+ next-value 1) 1) 
                   value 
                   (try next-value)))) 
           (try 1.0))) 
        4)) 
   ;; imported procedures from rational package 
   (define (numer x) ((get 'numer '(rational)) x)) 
   (define (denom x) ((get 'denom '(rational)) x)) 
   ;; internal procedures 
   (define (=zero? x) 
     (and (<= (abs (real-part x)) tolerance) (<= (abs (imag-part x)) tolerance))) 
   (define (equ? x y) (=zero? (sub x y))) 
   ;; interface to rest of the system 
   (put 'real-part '(scheme-number) (lambda (x) x)) 
   (put 'imag-part '(scheme-number) (lambda (x) 0)) 
   (put 'real-part '(rational) (lambda (x) (/ (numer x) (denom x)))) 
   (put 'imag-part '(rational) (lambda (x) 0)) 
  
   (put 'equ? 'generic equ?) 
   'done) 
  
 ;; TESTING 
  
 (display (equ? (make-scheme-number (/ 1 3)) 
                (make-scheme-number 0.333333333333333333))) 
 (display (equ? (make-rational 2 11) (make-rational 2 10)))  
 (display (equ? (make-complex-from-mag-ang 5 (asin 0.8)) 
                (make-complex-from-real-imag 3 4))) 
 (display (equ? (make-rational 8.881784197001252e-16 1) (make-rational 0 1))) 
 (display (equ? (make-complex-from-mag-ang 5 2)  
                (make-complex-from-mag-ang 5 (+ 2 (* 2 pi))))) 
  
 ;; output will be #t#f#t#t#t 


The above depends on numer and denom being exposed by the rational package, which they are not.

As for "breaking additivity", new types of numbers can be added at any time. The system as designed does not support adding arbirtary fundamental operations to an existing number type that depend on knowledge of the underlying implementation. One would have to either expose lower-level internals to the entire system or implement new operations by explicitly dealing with details of the implementation of a number type.

With the former, one is now mixing levels of abstraction. With the latter, one now has two implementations of a number type, which is brittle: What happens when the package's internal implementation changes? Leaving specific implementations to the specific packages makes the most sense to me.

Liskov

Thanks for your comment. I disagree that there are two implementations of numbers on my solution. The underlying representation of the number types remains the same.

"What happens when the package's internal implementation changes?"

It doesn't matter as long as I'm using the "public interface" or the data abstraction - but 'numer' and 'denom' are not being exposed. Well, then there is no solution without modifying the original packages.

The problem is that I forgot something: the generic arithmetic package is a superpackage of rational, complex and scheme number packages. (see figure 2.23).

Therefore, when the exercise ask to install the predicate in the 'generic arithmetic package' it isn't imposing a restriction. I think the original packages should be modified, and the first solution, which puts an 'equ?' procedure in each subpackage, is correct.