2012-02-25

SICP Exercise 2.82: Multi-Argument Coercion

Show how to generalize apply-generic to handle coercion in the general case of multiple arguments. One strategy is to attempt to coerce all the arguments to the type of the first argument, then to the type of the second argument, and so on. Give an example of a situation where this strategy (and likewise the two-argument version given above) is not sufficiently general. (Hint: Consider the case where there are some suitable mixed-type operations present in the table that will not be tried.)

Let's use the strategy suggested as the basis of our implementation.

In order to implement this we'll need a procedure that can coerce a list of arguments to a particular type, or provides a clear indication when this is not possible, so let's start there.

We can implement such a procedure iteratively, trying to coerce each argument in turn and accumulating a list of coerced arguments as we go. We'll need to append a list containing the newly-coerced argument at each step, as we want the arguments to end up in the same order as they were originally! This may lead you to question as to why we wouldn't do this recursively using cons to build the resulting list. We could do but, IMNSHO, the iterative approach allows us to provide a cleaner API when we fail to coerce an argument:
  • With an iterative implementation if we reach an argument that we fail to coerce we can simply discard the accumulated result so far and instead return a special error value such as #f. Callers can then easily check whether or not the coercion succeeded.
  • With a recursive implementation if we reach an argument that we fail to coerce the best we could do would be to return '() immediately instead of processing the rest of the arguments. The chain of recursive calls so far would still cons their successfully coerced arguments onto this and so the result of the procedure evaluation would be a list containing all of the coerced arguments preceding the failed one. Callers would then need to check the length of the returned list against the expected length to see whether or not the coercion succeeded.
To reduce the nesting in the implementation we'll take advantage of let*, a variant of let that performs the bindings of value to name in sequential order, allowing later bindings to use earlier bindings.

We'll also check whether or not an argument is already of the required type before trying to coerce it. If it's already of the required type then we don't need to perform any coercion.

Here's the implementation:
(define (coerce-to target-type remaining-args result)
  (if (null? remaining-args)
      result
      (let* ((arg (car remaining-args))
             (original-type (type-tag arg)))
        (if (eq? original-type target-type)
            (coerce-to target-type
                       (cdr remaining-args)
                       (append result (list arg)))
            (let ((original->target (get-coercion (type-tag arg) target-type)))
              (if original->target
                  (coerce-to target-type
                             (cdr remaining-args)
                             (append result (list (original->target arg))))
                  #f))))))
In order to test this out we'll need to implement the get-coercion and put-coercion procedures. We can do this for now by simply reserving an operation name in our table for coercion procedures ('coercion will do) and then wrapping the put and get procedures appropriately:
(define (put-coercion source-type target-type proc)
  (put 'coercion (list source-type target-type) proc))

(define (get-coercion source-type target-type)
  (get 'coercion (list source-type target-type)))
Having done that we can install the example scheme-number->complex coercion procedure from the Coercion section and then test our coerce-to procedure:
> (coerce-to 'complex
           (list (make-scheme-number 1)
                 (make-scheme-number 2)
                 (make-scheme-number 3))
           '())
((complex rectangular 1 . 0) (complex rectangular 2 . 0) (complex rectangular 3 . 0))
> (coerce-to 'complex
           (list (make-scheme-number 1)
                 (make-complex-from-real-imag 2 1)
                 (make-scheme-number 3))
           '())
((complex rectangular 1 . 0) (complex rectangular 2 . 1) (complex rectangular 3 . 0))
> (coerce-to 'complex
           (list (make-rational 1 2)
                 (make-complex-from-real-imag 2 1)
                 (make-scheme-number 3))
           '())
#f
That all looks good, so let's move on to using this to try to coerce the arguments to a type for which there is a suitable procedure to evaluate. If we assume we have a list of types to try out then what we want to do is to iterate through the types until we find one for which we can coerce all the arguments to that type and it is possible to get a corresponding procedure for arguments of that type. If we manage to find such a type then we'll evaluate the procedure with the coerced arguments, otherwise we'll raise an error.

The procedure itself will end up as an inner procedure of our updated apply-generic, so both the op and args arguments of apply-generic will be in scope. Bearing that in mind we can express this procedure as follows:
(define (apply-generic-iter coercion-types)
  (if (null? coercion-types)
      (error "No method for these types, and could not coerce"
             (list op (map type-tag args)))
      (let ((coerced-args (coerce-to (car coercion-types) args '())))
        (if coerced-args
            (let ((proc (get op (map type-tag coerced-args))))
              (if proc
                  (apply proc (map contents coerced-args))
                  (apply-generic-iter (cdr coercion-types))))
            (apply-generic-iter (cdr coercion-types))))))
To test this we'll first extend the complex package to include a new operation, addd, which adds three complex numbers together:
(define (install-complex-package)
  ;; imported procedures from rectangular and polar packages
  …
  ;; internal procedures
  …
  (define (addd-complex z1 z2 z3)
    (make-from-real-imag (+ (complex-real-part z1)
                            (complex-real-part z2)
                            (complex-real-part z3))
                         (+ (complex-imag-part z1)
                            (complex-imag-part z2)
                            (complex-imag-part z3))))
  …
  ;; interface to rest of the system
  …
  (put 'addd '(complex complex complex)
       (lambda (z1 z2 z3) (tag (addd-complex z1 z2 z3))))
  …
  'done)
Next we'll define the generic procedure addd:
(define (addd x y z) (apply-generic 'addd x y z))
Finally we can test this by defining op and args appropriately:
> (define op 'addd)
> (define args (list (make-scheme-number 1)
                     (make-scheme-number 2)
                     (make-scheme-number 3)))
> (apply-generic-iter '(scheme-number complex))
(complex rectangular 6 . 0)
> (define args (list (make-scheme-number 1)
                     (make-complex-from-real-imag 2 1)
                     (make-scheme-number 3)))
> (apply-generic-iter '(scheme-number complex))
(complex rectangular 6 . 1)
> (define args (list (make-complex-from-real-imag 1 2)
                     (make-scheme-number 3)
                     (make-complex-from-real-imag 4 5)))
> (apply-generic-iter '(scheme-number complex))
(complex rectangular 8 . 7)
Good stuff!

Now we're almost ready to put all this together... But not quite. First we should think about how we get the list of types to call this with?

The exercise suggests that we "attempt to coerce all the arguments to the type of the first argument, then to the type of the second argument, and so on." We know that we can get a list of the types of the arguments we're called with by evaluating (map type-tag args). We could then just iterate through that list and try coercing the arguments to each one in turn. However, this would be wasteful in many cases. If there are multiple arguments of the same type then we'll end up trying to perform the coercion to that type and, if it's successful, try to get the corresponding procedure multiple times.

So how can we avoid this? Well obviously it would be more efficient if we only tried each type once. So let's build ourselves a procedure that can take a list and throw out any duplicates. The resulting order isn't too important, so let's define its behaviour as follows: given a list the procedure will cons the head of the list onto the result of uniquifying the tail of the list iff the tail does not contain the head item. This results in the ordering of the unique list reflecting the ordering of the last occurrence of each item.

Here's the procedure:
  (define (uniquify l)
    (if (null? l)
        '()
        (let ((head (car l))
              (tail (cdr l)))
          (if (memq head tail)
              (uniquify tail)
              (cons head (uniquify tail))))))
...and here it is in action:
> (uniquify '(1 2 3 4 3 2 1))
(4 3 2 1)
> (uniquify '(1 2 1 3 1 4 1))
(2 3 4 1)
> (uniquify '(2 2 2 4 2 3 1 3))
(4 2 1 3)
Okay, finally we can put it together, right? We've got a procedure that tries to coerce a list of arguments to a particular type and we've got a procedure that iterates through a list of types, coercing the arguments to each type in turn until it finds a procedure it can evaluate for that type. We've even got a procedure that will enable us to ensure we only try each type once. So we're good to go, right?

Well, almost. There's just one more thing I'd like to consider.

Suppose we get called with a list of arguments all of the same type. We'll try to coerce them to that type which, while it will succeed, is not a necessary operation. So before we start any coercion let's first check to see if there's a procedure installed for the un-coerced types. If there is then we can evaluate it directly. If there isn't then we have to go ahead with extracting the list of types, uniquify it and, if the resulting type list has more than one item in it, try to find an appropriate installed procedure via type coercion.

Why only go ahead with trying to coerce if the uniquified type list has more than one item in it? Well if the type list only has one item in it then all the arguments must be of the same type: the one type in the type list. As a result our coercion procedure will not change the types of any of the arguments and so if a procedure existed for such a type list then our initial check for an appropriate installed procedure should have succeeded.

Oh, and this has the added advantage that if the argument list is of mixed type and there just happens to be a procedure installed that corresponds to that list of types then we'll invoke that. That wouldn't have happened if we'd followed the exercises suggested strategy to the letter.

Right, now we can put it all together:
(define (apply-generic op . args)
  (define (uniquify l)
    (if (null? l)
        '()
        (let ((head (car l))
              (tail (cdr l)))
          (if (memq head tail)
              (uniquify tail)
              (cons head (uniquify tail))))))
  (define (coerce-to target-type remaining-args result)
    (if (null? remaining-args)
        result
        (let* ((arg (car remaining-args))
               (original-type (type-tag arg)))
          (if (eq? original-type target-type)
              (coerce-to target-type
                         (cdr remaining-args)
                         (append result (list arg)))
              (let ((original->target (get-coercion (type-tag arg) target-type)))
                (if original->target
                    (coerce-to target-type
                               (cdr remaining-args)
                               (append result (list (original->target arg))))
                    #f))))))
  (define (apply-generic-iter coercion-types)
    (if (null? coercion-types)
        (error "No method for these types, and could not coerce"
               (list op (map type-tag args)))
        (let ((coerced-args (coerce-to (car coercion-types) args '())))
          (if coerced-args
              (let ((proc (get op (map type-tag coerced-args))))
                (if proc
                    (apply proc (map contents coerced-args))
                    (apply-generic-iter (cdr coercion-types))))
              (apply-generic-iter (cdr coercion-types))))))
  (let* ((type-tags (map type-tag args))
         (proc (get op type-tags)))
    (if proc
        (apply proc (map contents args))
        (let ((unique-types (uniquify type-tags)))
          (if (> (length unique-types) 1)
              (apply-generic-iter unique-types)
              (else (error "No method for this type"
                           (list op type-tags))))))))
Let's see this in action with addd:
> (addd (make-scheme-number 1)
        (make-complex-from-real-imag 2 1)
        (make-scheme-number 3))
(complex rectangular 6 . 1)
> (addd (make-complex-from-real-imag 1 2)
        (make-scheme-number 3)
        (make-complex-from-real-imag 4 5))
(complex rectangular 8 . 7)
Okay, so there's the updated apply-generic.

The exercise also asks us to "Give an example of a situation where this strategy (and likewise the two-argument version given above) is not sufficiently general". Well there are two situations I can think of where this approach will fail:
  1. While our implementation will successfully work with mixed-type procedures where there is a procedure installed for the types of the arguments that apply-generic is called with, it will fail to test for installed procedures of other mixed types. For example, suppose there is a procedure installed for the argument type list of '(complex scheme-number) then a call to apply-generic with arguments with the corresponding type list of '(scheme-number complex) will fail, even if it is possible to coerce the individual arguments to the appropriate types.
  2. Our implementation will only try to coerce to a type if there is at least one argument of that type. At the moment our system has packages installed for the scheme-number, rational and complex types. However, only the complex package has a procedure installed that implements addd. If we try to evaluate addd with an argument list consisting of only types scheme-number and rational then apply-generic will never try to coerce the arguments to the complex type and so will never find the installed procedure.

2012-02-08

SICP Exercise 2.81: Coercing to the Same Type

Louis Reasoner has noticed that apply-generic may try to coerce the arguments to each other's type even if they already have the same type. Therefore, he reasons, we need to put procedures in the coercion table to "coerce" arguments of each type to their own type. For example, in addition to the scheme-number->complex coercion shown above, he would do:
(define (scheme-number->scheme-number n) n)
(define (complex->complex z) z)
(put-coercion 'scheme-number 'scheme-number
              scheme-number->scheme-number)
(put-coercion 'complex 'complex complex->complex)
  1. With Louis's coercion procedures installed, what happens if apply-generic is called with two arguments of type scheme-number or two arguments of type complex for an operation that is not found in the table for those types? For example, assume that we've defined a generic exponentiation operation:
    (define (exp x y) (apply-generic 'exp x y))
    
    and have put a procedure for exponentiation in the Scheme-number package but not in any other package:
    ;; following added to Scheme-number package
    (put 'exp '(scheme-number scheme-number)
         (lambda (x y) (tag (expt x y)))) ; using primitive expt
    
    What happens if we call exp with two complex numbers as arguments?
  2. Is Louis correct that something had to be done about coercion with arguments of the same type, or does apply-generic work correctly as is?
  3. Modify apply-generic so that it doesn't try coercion if the two arguments have the same type.

(a) Same-Type Arguments, No Matching Operation - What Happens?

Let's do a manual evaluation to see what happens! First, let's start by defining a couple of complex numbers to work with:
(define z1 (make-complex-from-real-imag 1 2))
(define z2 (make-complex-from-real-imag 3 4))
Now let's evaluate (exp z1 z2). As we've done before we'll simplify this evaluation a bit by skipping the details of map and get.
  (exp z1 z2)
= (apply-generic 'exp z1 z2)
= (let ((type-tags (map type-tag (list z1 z2))))
    (let ((proc (get 'exp type-tags)))
      (if proc
          (apply proc (map contents (list z1 z2)))
          (if (= (length (list z1 z2)) 2)
              (let ((type1 (car type-tags))
                    (type2 (cadr type-tags))
                    (a1 (car (list z1 z2)))
                    (a2 (cadr (list z1 z2))))
                (let ((t1->t2 (get-coercion type1 type2))
                      (t2->t1 (get-coercion type2 type1)))
                  (cond (t1->t2
                         (apply-generic 'exp (t1->t2 a1) a2))
                        (t2->t1
                         (apply-generic 'exp a1 (t2->t1 a2)))
                        (else
                         (error "No method for these types"
                                (list 'exp type-tags))))))
              (error "No method for these types"
                     (list 'exp type-tags))))))
= (let ((type-tags '(complex complex)))
    (let ((proc (get 'exp type-tags)))
      (if proc
          (apply proc (map contents (list z1 z2)))
          (if (= (length (list z1 z2)) 2)
              (let ((type1 (car type-tags))
                    (type2 (cadr type-tags))
                    (a1 (car (list z1 z2)))
                    (a2 (cadr (list z1 z2))))
                (let ((t1->t2 (get-coercion type1 type2))
                      (t2->t1 (get-coercion type2 type1)))
                  (cond (t1->t2
                         (apply-generic 'exp (t1->t2 a1) a2))
                        (t2->t1
                         (apply-generic 'exp a1 (t2->t1 a2)))
                        (else
                         (error "No method for these types"
                                (list 'exp type-tags))))))
              (error "No method for these types"
                     (list 'exp type-tags))))))
= (let ((proc (get 'exp '(complex complex))))
    (if proc
        (apply proc (map contents (list z1 z2)))
        (if (= (length (list z1 z2)) 2)
            (let ((type1 (car '(complex complex)))
                  (type2 (cadr '(complex complex)))
                  (a1 (car (list z1 z2)))
                  (a2 (cadr (list z1 z2))))
              (let ((t1->t2 (get-coercion type1 type2))
                    (t2->t1 (get-coercion type2 type1)))
                (cond (t1->t2
                       (apply-generic 'exp (t1->t2 a1) a2))
                      (t2->t1
                       (apply-generic 'exp a1 (t2->t1 a2)))
                      (else
                       (error "No method for these types"
                              (list 'exp '(complex complex)))))))
            (error "No method for these types"
                   (list 'exp '(complex complex))))))
= (let ((proc false))
    (if proc
        (apply proc (map contents (list z1 z2)))
        (if (= (length (list z1 z2)) 2)
            (let ((type1 (car '(complex complex)))
                  (type2 (cadr '(complex complex)))
                  (a1 (car (list z1 z2)))
                  (a2 (cadr (list z1 z2))))
              (let ((t1->t2 (get-coercion type1 type2))
                    (t2->t1 (get-coercion type2 type1)))
                (cond (t1->t2
                       (apply-generic 'exp (t1->t2 a1) a2))
                      (t2->t1
                       (apply-generic 'exp a1 (t2->t1 a2)))
                      (else
                       (error "No method for these types"
                              (list 'exp '(complex complex)))))))
            (error "No method for these types"
                   (list 'exp '(complex complex))))))
= (if false
      (apply false (map contents (list z1 z2)))
      (if (= (length (list z1 z2)) 2)
          (let ((type1 (car '(complex complex)))
                (type2 (cadr '(complex complex)))
                (a1 (car (list z1 z2)))
                (a2 (cadr (list z1 z2))))
            (let ((t1->t2 (get-coercion type1 type2))
                  (t2->t1 (get-coercion type2 type1)))
              (cond (t1->t2
                     (apply-generic 'exp (t1->t2 a1) a2))
                    (t2->t1
                     (apply-generic 'exp a1 (t2->t1 a2)))
                    (else
                     (error "No method for these types"
                            (list 'exp '(complex complex)))))))
          (error "No method for these types"
                 (list 'exp '(complex complex)))))
= (if (= (length (list z1 z2)) 2)
      (let ((type1 (car '(complex complex)))
            (type2 (cadr '(complex complex)))
            (a1 (car (list z1 z2)))
            (a2 (cadr (list z1 z2))))
        (let ((t1->t2 (get-coercion type1 type2))
              (t2->t1 (get-coercion type2 type1)))
          (cond (t1->t2
                 (apply-generic 'exp (t1->t2 a1) a2))
                (t2->t1
                 (apply-generic 'exp a1 (t2->t1 a2)))
                (else
                 (error "No method for these types"
                        (list 'exp '(complex complex)))))))
      (error "No method for these types"
             (list 'exp '(complex complex)))))
= (if (= 2 2)
      (let ((type1 (car '(complex complex)))
            (type2 (cadr '(complex complex)))
            (a1 (car (list z1 z2)))
            (a2 (cadr (list z1 z2))))
        (let ((t1->t2 (get-coercion type1 type2))
              (t2->t1 (get-coercion type2 type1)))
          (cond (t1->t2
                 (apply-generic 'exp (t1->t2 a1) a2))
                (t2->t1
                 (apply-generic 'exp a1 (t2->t1 a2)))
                (else
                 (error "No method for these types"
                        (list 'exp '(complex complex)))))))
      (error "No method for these types"
             (list 'exp '(complex complex)))))
= (if true
      (let ((type1 (car '(complex complex)))
            (type2 (cadr '(complex complex)))
            (a1 (car (list z1 z2)))
            (a2 (cadr (list z1 z2))))
        (let ((t1->t2 (get-coercion type1 type2))
              (t2->t1 (get-coercion type2 type1)))
          (cond (t1->t2
                 (apply-generic 'exp (t1->t2 a1) a2))
                (t2->t1
                 (apply-generic 'exp a1 (t2->t1 a2)))
                (else
                 (error "No method for these types"
                        (list 'exp '(complex complex)))))))
      (error "No method for these types"
             (list 'exp '(complex complex)))))
= (let ((type1 (car '(complex complex)))
        (type2 (cadr '(complex complex)))
        (a1 (car (list z1 z2)))
        (a2 (cadr (list z1 z2))))
    (let ((t1->t2 (get-coercion type1 type2))
          (t2->t1 (get-coercion type2 type1)))
      (cond (t1->t2
             (apply-generic 'exp (t1->t2 a1) a2))
            (t2->t1
             (apply-generic 'exp a1 (t2->t1 a2)))
            (else
             (error "No method for these types"
                    (list 'exp '(complex complex)))))))
= (let ((type1 'complex)
        (type2 'complex)
        (a1 z1)
        (a2 z2))
    (let ((t1->t2 (get-coercion type1 type2))
          (t2->t1 (get-coercion type2 type1)))
      (cond (t1->t2
             (apply-generic 'exp (t1->t2 a1) a2))
            (t2->t1
             (apply-generic 'exp a1 (t2->t1 a2)))
            (else
             (error "No method for these types"
                    (list 'exp '(complex complex)))))))
= (let ((t1->t2 (get-coercion 'complex 'complex))
        (t2->t1 (get-coercion 'complex 'complex)))
    (cond (t1->t2
           (apply-generic 'exp (t1->t2 z1) z2))
          (t2->t1
           (apply-generic 'exp z1 (t2->t1 z2)))
          (else
           (error "No method for these types"
                  (list 'exp '(complex complex))))))
= (let ((t1->t2 complex->complex)
        (t2->t1 complex->complex))
    (cond (t1->t2
           (apply-generic 'exp (t1->t2 z1) z2))
          (t2->t1
           (apply-generic 'exp z1 (t2->t1 z2)))
          (else
           (error "No method for these types"
                  (list 'exp '(complex complex))))))
= (cond (complex->complex
         (apply-generic 'exp (complex->complex z1) z2))
        (complex->complex
         (apply-generic 'exp z1 (complex->complex z2)))
        (else
         (error "No method for these types"
                (list 'exp '(complex complex)))))
= (apply-generic 'exp (complex->complex z1) z2)
= (apply-generic 'exp z1 z2)
At this point we'll stop the evaluation. We're about evaluate (apply-generic 'exp z1 z2). Now if you recall (or if you have a quick peak back up at the start of the evaluation), this matches the second step in our evaluation. This means that we're about to enter an infinite loop!

So there's our answer. If we install same-type coercion procedures in the system then, if no matching procedure can be found for an operation called with a pair of same-type arguments, apply-generic will enter an infinite evaluation loop.

(b) Do We Need Same-Type Coercion?

Strictly speaking, no we don't need to do anything about coercion with arguments of the same type. If an operation is not found in the table for same-type arguments then apply-generic will correctly raise an error.

Let's prove this to ourselves by tracing through the same example as in part (a), but assuming that Louis has not installed the same-type coercion procedure for complex numbers. We'll skip a large number of the initial steps, as they're identical to the evaluation above, and jump in at the point where we try to obtain the coercion procedures.
  (exp z1 z2)
= (apply-generic 'exp z1 z2)
= …
= (let ((t1->t2 (get-coercion 'complex 'complex))
        (t2->t1 (get-coercion 'complex 'complex)))
    (cond (t1->t2
           (apply-generic 'exp (t1->t2 z1) z2))
          (t2->t1
           (apply-generic 'exp z1 (t2->t1 z2)))
          (else
           (error "No method for these types"
                  (list 'exp '(complex complex))))))
= (let ((t1->t2 false)
        (t2->t1 false))
    (cond (t1->t2
           (apply-generic 'exp (t1->t2 z1) z2))
          (t2->t1
           (apply-generic 'exp z1 (t2->t1 z2)))
          (else
           (error "No method for these types"
                  (list 'exp '(complex complex))))))
= (cond (false
         (apply-generic 'exp (t1->t2 z1) z2))
        (false
         (apply-generic 'exp z1 (t2->t1 z2)))
        (else
         (error "No method for these types"
                (list 'exp '(complex complex)))))
= (error "No method for these types"
         (list 'exp '(complex complex)))
So, as you can see, we get the required behaviour.

Note that I said "strictly speaking" above. The procedure is functionally correct. However, it does perform a number of unnecessary operations, particularly extracting the individual arguments and trying to look up coercion procedures. By eliminating these steps we could make the procedure slightly more efficient in this case.

If only we had some excuse to do this.

Oh look, it's part (c)...

(c) Removing Same-Type Coercion from apply-generic

In order to prevent coercion of same-type arguments we can modify apply-generic so that, after it has determined that there are two arguments and it has extracted the types of those arguments, it checks to see if they are of the same type or not before proceeding. If they are then it should raise an error, otherwise it should continue as before.

Programmatically this can be expressed as:
(define (apply-generic op . args)
  (let ((type-tags (map type-tag args)))
    (let ((proc (get op type-tags)))
      (if proc
          (apply proc (map contents args))
          (if (= (length args) 2)
              (let ((type1 (car type-tags))
                    (type2 (cadr type-tags)))
                (if (not (eq? type1 type2))
                    (let ((t1->t2 (get-coercion type1 type2))
                          (t2->t1 (get-coercion type2 type1))
                          (a1 (car args))
                          (a2 (cadr args)))
                      (cond (t1->t2
                             (apply-generic op (t1->t2 a1) a2))
                            (t2->t1
                             (apply-generic op a1 (t2->t1 a2)))
                            (else
                             (error "No method for these types"
                                    (list op type-tags)))))
                    (error "No method for these types"
                                    (list op type-tags))))
              (error "No method for these types"
                     (list op type-tags)))))))

2012-02-04

SICP Exercise 2.80: Generic Zero Testing

Define a generic predicate =zero? that tests if its argument is zero, and install it in the generic arithmetic package. This operation should work for ordinary numbers, rational numbers, and complex numbers.

Well, we've just written equ?, so we could get away with simply adding procedures to each package that test for equality using the type-specific equ? procedure, setting one of the values to the type's equivalent of zero. I.e. 0 for scheme-number, (make-rat 0 1) for rational and either (make-complex-from-real-imag 0 0) or (make-complex-from-mag-ang 0 0) for complex.

However, for rational and complex numbers we can have a slightly simpler test. Note that any rational number that is equal to zero will have a numerator of 0. Similarly, any complex number that is equal to zero will have a magnitude of 0. Using this approach we don't need to test the denominator for rational numbers, or the angle (or rectangular form components) for complex numbers.

Here's the generic operation and the modifications I made to the packages:
(define (=zero? x) (apply-generic '=zero? x))

(define (install-scheme-number-package)
  …
  (put '=zero? '(scheme-number)
       (lambda (x) (= 0 x)))
  …
  'done)

(define (install-rational-package)
  ;; internal procedures
  …
  (define (=zero? x) (= (numer x) 0))
  …
  ;; interface to rest of the system
  …
  (put '=zero? '(rational) =zero?)
  …
  'done)

(define (install-complex-package)
  ;; imported procedures from rectangular and polar packages
  …
  ;; internal procedures
  …
  (define (=zero? x) (= (complex-magnitude x) 0))
  …
  ;; interface to rest of the system
  …
  (put '=zero? '(complex) =zero?)
  …
  'done)
Similar to the last exercise, if you're wondering about complex-magnitude then have a read of my solution to exercise 2.77.

Let's give it a spin:
> (=zero? (make-scheme-number 4))
#f
> (=zero? (sub (make-scheme-number 3) (make-scheme-number 3)))
#t
> (=zero? (sub (make-rational 1 2) (make-rational 3 2)))
#f
> (=zero? (add (make-rational 1 2) (make-rational -2 4)))
#t
> (=zero? (make-complex-from-real-imag 0 0))
#t
> (=zero? (make-complex-from-mag-ang 0 42))
#t

SICP Exercise 2.79: Generic Equality Testing

Define a generic equality predicate equ? that tests the equality of two numbers, and install it in the generic arithmetic package. This operation should work for ordinary numbers, rational numbers, and complex numbers.

Before we start on figuring out how to test for equality, we should first note a couple of things:
  • We're going to build a generic equality predicate here. Predicate is defined in section 1.1.6 as: "used for procedures that return true or false, as well as for expressions that evaluate to true or false." So, unlike all of the procedures defined so far in the packages, a package's implementation of equ? will not be returning a data object of the package's type. As a result we won't need to tag the result of the predicate. This means we can install these procedures directly into the operations table, rather than wrapping it in a λ-function that performs the tagging.
  • We're not going to deal with coercion until the next section. As a result we can assume that "This operation should work for ordinary numbers, rational numbers, and complex numbers" means that the operation should work for any pair of numbers of the same type, but that it doesn't need to work for pairs of numbers of different types. This is identical to the behaviour of the existing generic operations.
Okay, so onto the exercise itself...

Firstly, the generic operation itself is straightforward, following the same pattern as the other generic operations:
(define (equ? x y) (apply-generic 'equ? x y))
Next, let's deal with the scheme-number package. This package uses primitive Scheme numbers as its untagged representation and, as the internal procedure we need here will be dealing with the untagged representation, this means that we can use the primitive = procedure to perform the equality test. This can be installed directly into the operations table without a surrounding λ-function because, as noted above, we don't need to mutate the result of this.

Onto the rational package... The set of rational numbers can be formally defined as sets of equivalence classes where each equivalence class is of infinite size. This means that testing for rational number equality normally takes a little bit more than simply checking that the numerators are equal and the denominators are equal; you need to cope with mathematically equal numbers that have different numerators and denominators (such as 1/2 and 2/4). The usual way of achieving this is, for any given pair of rational numbers, n1/d1 and n2/d2, testing for equality by testing that n1d2 = n2d1 holds. However, as make-rat reduces any rational number to its canonical representation we can ignore this necessity and do the simple check.

Finally, let's consider the complex package. We have two internal representations we can use for complex numbers: rectangular and polar form. In order to test for equality here it is enough to pick one of the forms and compare the components of that form for equality. So we can either pick rectangular form and so compare the real and imaginary components for equality, or we can pick polar form and so compare the magnitude and angle components for equality. Let's pick the rectangular form.

Okay, so we know how we're going to do it... Here's the changes we make:
(define (install-scheme-number-package)
  …
  (put 'equ? '(scheme-number scheme-number) =)
  …
  'done)

(define (install-rational-package)
  ;; internal procedures
  …
  (define (equ? x y)
    (and (= (numer x) (numer y))
         (= (denom x) (denom y))))
  …
  ;; interface to rest of the system
  …
  (put 'equ? '(rational rational) equ?)
  …
  'done)

(define (install-complex-package)
  ;; imported procedures from rectangular and polar packages
  …
  ;; internal procedures
  …
  (define (equ? z1 z2)
    (and (= (complex-real-part z1) (complex-real-part z2))
         (= (complex-imag-part z1) (complex-imag-part z2))))
  …
  ;; interface to rest of the system
  …
  (put 'equ? '(complex complex) equ?)
  …
  'done)
If you're wondering about the complex- prefix to real-part and imag-part, then have a read of my solution to exercise 2.77 - there's a name clash with the R6RS complex number support.

Okay, so let's see this in action:
> (equ? (make-scheme-number 3) (make-scheme-number 4))
#f
> (equ? (make-scheme-number 5) (make-scheme-number 5))
#t
> (equ? (make-rational 2 3) (make-rational 3 5))
#f
> (equ? (make-rational 1 2) (make-rational 2 4))
#t
> (equ? (make-complex-from-real-imag 1 2) (make-complex-from-real-imag 3 4))
#f
> (equ? (make-complex-from-real-imag 5 6) (make-complex-from-real-imag 5 6))
#t
> (equ? (make-complex-from-mag-ang 2 4) (make-complex-from-mag-ang 6 8))
#f
> (equ? (make-complex-from-mag-ang 1 3) (make-complex-from-mag-ang 1 3))
#t
> (equ? (make-complex-from-real-imag 3 0) (make-complex-from-mag-ang 3 0))
#t

2012-02-02

SICP Exercise 2.78: Using Primitive Types

The internal procedures in the scheme-number package are essentially nothing more than calls to the primitive procedures +, -, etc. It was not possible to use the primitives of the language directly because our type-tag system requires that each data object have a type attached to it. In fact, however, all Lisp implementations do have a type system, which they use internally. Primitive predicates such as symbol? and number? determine whether data objects have particular types. Modify the definitions of type-tag, contents, and attach-tag from section 2.4.2 so that our generic system takes advantage of Scheme's internal type system. That is to say, the system should work as before except that ordinary numbers should be represented simply as Scheme numbers rather than as pairs whose car is the symbol scheme-number.

To recap, here are the original implementations of the three procedures in question:
(define (attach-tag type-tag contents)
  (cons type-tag contents))
(define (type-tag datum)
  (if (pair? datum)
      (car datum)
      (error "Bad tagged datum -- TYPE-TAG" datum)))
(define (contents datum)
  (if (pair? datum)
      (cdr datum)
      (error "Bad tagged datum -- CONTENTS" datum)))
What we want to do is to modify these so that they treat any contents or datum that is a primitive number specially.

The changes to type-tag and contents are straightforward. We simply extend them both so that, in addition to checking whether datum is a pair and handling that as is currently done, they also check whether it's a number. If a number is encountered then, rather than raise an error as would currently happen, type-tag should return 'scheme-number while contents should return the datum directly.

attach-tag raises an interesting issue. Now the obvious solution is simply to modify it so that it tests whether the passed contents is a number or not. If it is then simply return the number; if it isn't then return a tagged pair (i.e. the current behaviour). However, this is not necessarily the correct behaviour.

Our system is supposed to be additive, supporting new number representations. What if I decide to add a new package for representing transfinite cardinal numbers, with the tag 'transfinite-cardinal, and the representation being a non-negative primitive Scheme integer such that 0 represents ℵ0, 1 represents ℵ1, and so on? If I make the obvious modification to attach-tag then attach-tag will ignore my type-tag, spot that the contents are a primitive Scheme number, return the untagged contents and break my transfinite cardinals package!

No, for attach-tag to work correctly, it really needs to check if type-tag is 'scheme-number rather than checking whether contents is a primitive Scheme number.

Given all that, here's how we can express this programmatically:
(define (attach-tag type-tag contents)
  (if (= type-tag 'scheme-number)
      contents
      (cons type-tag contents)))
(define (type-tag datum)
  (cond ((number? datum) 'scheme-number)
        ((pair? datum) (car datum))
        (else (error "Bad tagged datum -- TYPE-TAG" datum))))
(define (contents datum)
  (cond ((number? datum) datum)
        ((pair? datum) (cdr datum))
        (else (error "Bad tagged datum -- CONTENTS" datum))))
If we swap these in for our original three procedures then we can give it all a spin:
> (attach-tag 'scheme-number 5)
5
> (attach-tag 'transfinite-cardinal 2)
(mcons 'transfinite-cardinal 2)
> (type-tag 4)
'scheme-number
> (contents 3)
3
> (type-tag (attach-tag 'transfinite-cardinal 2))
'transfinite-cardinal
> (contents (make-rational 5 4))
(mcons 5 4)
> (add (make-scheme-number 5) (make-scheme-number 4))
9
> (add (make-rational 1 3) (make-rational 1 4))
(mcons 'rational (mcons 7 12))
All seems to be in order... If you're wondering about the mcons then you should read part (b) of my solution to exercise 2.73.