SICP 2.5 Systems with Generic Operations
2023-11-15 Wed
2.5.1 Generic Arithmetic Operations
In 2.5.1, Authors show how to define generic operations that can take
different types of arguments (in our case different types of numbers),
for example a generic add
that works with ordinary numbers as well
as with rational and complex numbers. The technique used to define
such generic operations is the same way that has been used to define
the generic operations (selectors) in the case of complex numbers (in
2.4.3).
Exercise 2.77
Exercise:
Louis Reasoner tries to evaluate the expression
(magnitude z)
wherez
is the object shown in Figure 2-24. To his surprise, instead of the answer 5 he gets an error message fromapply-generic
, saying there is no method for the operationmagnitude
on the types(complex)
. He shows this interaction to Alyssa P. Hacker, who says "The problem is that the complex-number selectors were never defined forcomplex
numbers, just forpolar
andrectangular
numbers. All you have to do to make this work is add the following to thecomplex
package:"(put 'real-part '(complex) real-part) (put 'imag-part '(complex) imag-part) (put 'magnitude '(complex) magnitude) (put 'angle '(complex) angle)Describe in detail why this works. As an example, trace through all the procedures called in evaluating the expression
(magnitude z)
wherez
is the object shown in Figure 2-24. In particular, how many times isapply-generic
invoked? What procedure is dispatched to in each case?
Answer:
Here is z
:
-->[o|o]-->[o|o]-------->[o|o] | | | | v v v v 'complex 'rectangular 3 4
Louis evaluates (magnitude z)
.
Louis is using the procedure defined as follows:
(define (magnitude z) (apply-generic 'magnitude z))
This means that when calling magnitude
, the first thing we do is
looking in the table for the item specified by the row 'magnitude
and column 'complex
(the type of z
).
However, nobody has stored such a table item. (At the moment there is
an element specified by row 'magnitude
and column 'rectangular
,
and an element specified by row 'magnitude
and column 'polar
.) So,
Louis' invocation produces an error.
Alyssa's code adds to the table four objects. Those objects are the generic procedures defined on page 84, which are themselves designed to look for and use an object in the table.
So, now, when Louis calls (magnitude z)
, we look for a table item
which exists. It's the item which has been installed by Alyssa with
this line:
(put 'magnitude '(complex) magnitude)
Given that we find an item in the table, apply-generic
applies it to
the contents
of z
. These are the contents
of z
:
[o|o]------>[o|o] | | | v v v 'rectangular 3 4
Applying magnitude
to these contents means, again, looking for an
item in the table. This time we are looking for the item specified the
row 'magnitude
and the column 'rectangular
. That item is found and
applied to the contents.
Table before Alyssa's change:
rectangular | polar | |
---|---|---|
… | ||
magnitude | magnitude-rectangular | magnitude-polar |
… |
Table after Alyssa's change:
complex | rectangular | polar | |
---|---|---|---|
… | |||
magnitude | magnitude | magnitude-rectangular | magnitude-polar |
… |
When evaluating (magnitude z)
, after Alyssa's contribution,
apply-generic
is called twice. The first time it dispatches to
magnitude
to itself (in a sense, magnitude
, through
apply-generic
, is dispatching to itself). The second time it
dispatches to magnitude-rectangular
.
(magnitude z) ;; z: -->[o|o]-->[o|o]-------->[o|o] ;; | | | | ;; v v v v ;; 'complex 'rectangular 3 4 ;; | ;; | ;; V ;; apply-generic ;; | ;; | ;; V (magnitude z') ;; z': -->[o|o]-------->[o|o] ;; | | | ;; v v v ;; 'rectangular 3 4 ;; | ;; | ;; V ;; apply-generic ;; | ;; | ;; V (magnitude z'') ;; z'': -->[o|o] ;; | | ;; v v ;; 3 4
Basically, Alyssa's line, has the effect of making magnitude
stripping off 'complex
before dispatching to someone else.
Exercise 2.78
Exercise:
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 assymbol?
andnumber?
determine whether data objects have particular types. Modify the definitions oftype-tag
,contents
, andattach-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 whosecar
is the symbolscheme-number
.
Answer:
(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)))) (define (attach-tag type-tag contents) (if (number? contents) contents (cons type-tag contents))) ;; alternatively (define (attach-tag type-tag contents) (if (eq? type-tag 'scheme-number) contents (cons type-tag contents)))
Exercise 2.79
Exercise:
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.
Answer:
I guess, first of all:
(define (equ? x y) apply-generic 'equ? x y)
After this, we should put the specific procedures for the types of numbers we have:
(put 'equ? '(scheme-number scheme-number)
(lambda (x y) (= x y)))
(put 'equ? '(rational rational) (lambda (x y) (and (= (car x) (car y)) (= (cdr x) (cdr y)))))
(if equ?
for rational numbers was defined within the
install-rational-package
procedure we could make use of number
and
denom
)
(put 'equ? '(complex complex) (lambda (x y) (and (= (real-part x) (real-part y)) (= (imag-part x) (imag-part y)))))
(Real-part
and imag-part
, if I'm not wrong, here work correctly
thanks to the code added by Alyssa P. Hacker in ex. 2.77.)
(Alternatively, we could have used magnitude
and angle
, instead of
real-part
and imag-part
.)
Exercise 2.80
Exercise*:
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.
Answer:
(define (=zero? x) (apply-generic =zero? x))
(put '=zero '(scheme-number scheme-number)
(lambda (x) (= x 0)))
(put '=zero '(rational rational) (lambda (x) (and (= (car x) 0) (not (= (cdr x) 0))))) ;; the numerator must be zero and the denominator must be non-zero
(put '=zero '(complex) (lambda (x) (and (= (real-part x) 0) (= (imag-part x) 0))))
Alternatively:
(put '=zero '(complex)
(lambda (x)
(= (angle x) 0)))
2.5.2 Combining Data of Different Types
So far we have considered operations on objects of the same type. For example, the addition of two ordinary numbers or the multiplication of two rational numbers. But this means that the operations we have defined ``treat the different data types as being completely independent.''. We haven't dealt with, say the addition of an ordinary number and a rational number, or the division of a rational number and a complex number. How should we introduce ``cross-type'' operations in our system?
one way to handle cross-type operations is to design a different procedure for each possible combination of types for which the operation is valid. […] this technique works, but it is cumbersome.
When we can, we should be ``by taking advantage of additional structure that may be latent in our type system'': often an object of a certain data type can be seen as an object of a another data type. E.g., the rational number 2/2, can be seen as the ordinary number 1. Given so, if we are asked to perform an operation on a rational number and an ordinary number, we could try to ``coerce'' the rational number into an ordinary number. And if we are successful in doing so, then we can use our good old procedure that works with ordinary numbers.
The coercion idea can be implement by designing coercion procedures,
installing them into a coercion table, and then modifying
apply-generic
.
(define (scheme-number->complex n) (make-complex-from-real-imag (contents n) 0)) (put-coercion 'scheme-number 'complex scheme-number->complex) ;; For simplicity, only the case in which there are two arguments is ;; considered (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)) (a1 (car args)) (a2 (cadr args))) (let ((t1->t2 (get-coercion type1 type2)) (t2->t1 (get-coercion type2 type1))) (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)))))))
This coercion scheme is useful, but not general enough: there may be cases in which it's not possible neither object can be converted into the type of the other object, but in which both objects could be converted to a third type.
Exercise 2.81
Exercise:
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 thescheme-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)a. With Louis's coercion procedures installed, what happens if
apply-generic
is called with two arguments of typescheme-number
or two arguments of typecomplex
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?b. 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?c. Modify
apply-generic
so that it doesn't try coercion if the two arguments have the same type.
Answer:
a
If we call exp
with two complex numbers, we would call
(apply-generic 'exp CN1 CN2)
We are not going to find a procedure, because we don't have an object
in the table for row 'exp
and column '(complex complex)
.
So we would go inside the second if
.
Both t1->t2
and t2->t1
would be truthy, thanks to the code added
by Louis Reasoner.
Given that the former is truthy, we would call
(apply-generic 'exp (t1->t2 CN1) CN2)
(t1->t2 CN1)
evaluates to CN1, so we would call apply-generic
again with its original arguments. So: we would be calling
apply-generic
ad infinitum with the same arguments.
b
Think about what happens if we call exp
with two rational numbers or
complex numbers without the additional code provided by Louis
Reasoner.
Apply-generic
would not find a proc
(when it uses get
), and so
(given that (= (length args) 2)
is true) is going to apply
get-coercion
twice. So, yeah, Louis Reasoner was right. Something
has to be done.
c
My first instinct is to turn
(if (= (length args) 2)
into
(if (and (= (length args) 2) (eq (car (type-tags)) (cadr (type-tags))))
There might be more elegant ways of doing it, but this should work. Here is the modified procedure:
(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 (and (= (length args) 2) (eq (car (type-tags)) (cadr (type-tags)))) (let ((type1 (car type-tags)) (type2 (cadr type-tags)) (a1 (car args)) (a2 (cadr args))) (let ((t1->t2 (get-coercion type1 type2)) (t2->t1 (get-coercion type2 type1))) (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)))))))
Exercise 2.82
Exercise:
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.)
Answer:
Authors are telling us what to try: ``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.''
So we need to ``loop'' over the arguments and, for each one of them, we get its type and then try to coerce all the others to that type. This sort of ``double loop'' operation probably lends itself to be handled in some elegant way using higher-order procedures.
In the following approach, if the first retrieval of proc
fails, I'm
going to loop over each argument using what I would call ``procedural
iteration'' (Cf. pp. 32-33). I create the list of the functions needed
from the coercion table, and create, if possible (if I've found all
relevant procedures), the list of all coerced arguments. We then try
to retrieve the relevant proc
again. I we succeed we can call
apply
, otherwise we keep iterating.
(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)) (apply-generic-coerce 0 op . args))))) (define (apply-generic-coerce i op . args) (if (>= i (length args)) (error "failed to find op") ;; find type of args with index i (let ((type (type-tag (list-ref args i)))) ;; build a list of all the functions that are needed from the coercion table ;; (when the type is the same we can just use the identity function. (let ((coercing-funs (map (lambda (arg) (if (eq? (type-tag arg) type) identity (get-coercion (type-tag arg) type))) args))) (if (> (length (filter is-falsy coercing-funs) 0) ;; try with next type (apply-generic-coerce (+ 1 i) op . args) (let ((coerced-args (map (lambda (arg) ((get-coercion (type-tag arg) type) arg))))) (let ((proc (get op (map type-tags coerced-args)))) (if proc ;; found proc! (apply proc (map contents coerced-args)) ;; try with next type (apply-generic-coerce (+ 1 i) op . args))))))))))
I suspect that the situations in which the procedure is not going to
work are those situations in which, in order to find an operation in
our table, we would have to coerce each argument to some type which
differs from any of the types of the arguments. For example, if we
have in the table an operation foo
which works on triangles and we
try to apply it to an isosceles triangle and a right triangle, trying
to coerce the isosceles triangle to a right triangle or vice versa
will not work. We will have instead to coerce both the isosceles and
the right triangles to a triangle.