-------------------------------------------------- Today: Supporting operations on multiple types of data. called "Generic Operations" -- OPERATIONS X DATA TYPES There are three basic techniques for supporting generic operations: * Generic functions -- type dispatching, which we saw in the symbolic differentiation * Message Passing * Data Directed Lookup The first is supported by Swindle, and the one we will use most during the term. Probably also evolving as the most common method. -------------------------------------------------- Example: Complex number arithmetic Complex numbers represented as ordered pairs: z = x + iy or equivalently z = mag * e^(i*ang) = mag cis ang So, there are two natural representations: * Rectangular -- (x, y) * polar -- (mag, ang) We could just choose one, but each one has its own strengths: ADDITION: (x+iy) + (x'+iy') = (x+x') + i(y+y') -- add real and imaginary parts It's not so easy in polar coords MULTIPLICATION: (m cis a) * (m' cis a') = (m m') cis (a+a') -- multiply magnitudes and add angles A bit harder in rectangular coords. Thus we would like to have both forms: * Do we have to choose? Suppose we had constructors (make-complex-polar mag ang) --> z from mag and ang (polar) (make-complex-rect real imag) --> z from real and imag (rectangular) These constructors could produce two different types of numbers, in or form. We know how to define and using defstruct with slots for the two parts of each number. Then we would define GENERIC accessors that operate on either form: (real z) -- returns real part of *any* complex number. >>> Regardless of which way it was made same for: (imag z) (mag z) (ang z) These accessors would simply use the accessors or convert to the right form as appropriate. We can look at this in terms of the OPERATIONS x TYPES: [PUT THIS ON BOARD AND LEAVE UP:] TYPE: OP: ---------------------------------------- real | convert | accessor ---------------------------------------- imag | convert | accessor ---------------------------------------- mag | accessor | convert ---------------------------------------- ang | accessor | convert ---------------------------------------- How would we define the types? We want a type such that 's and 's are both . One way is to use the type hierarchy (in the same way that 's and 's are both ). (defstruct ) We refer to as an ABSTRACT TYPE, because it is not meant to be instantiated --- don't call make-complex. Some languages have support for declaring abstract types, so that they cannot be instantiated. [Swindle can also do that, but this is too much for now.] Then we define the classes and as having the parent (defstruct ( ) (real ) (imag )) (defstruct ( ) (mag ) (angle )) [DRAW THE TYPE TREE] With the standard way of constructing them using make-rectangular and make-polar. Given the generic operators -- which we'll have to build -- we can then implement complex number arithmetic quite easily: (define (mul-complex (x ) (y )) (make-polar (* (mag x) (mag y)) (+ (angle x) (angle y)))) (define (add-complex (x ) (y )) (make-rect (+ (real x) (real y)) (+ (imag x) (imag y)))) Subtraction and division are similar. ** So, we've built complex arithmetic on top of those constructors and selectors What is the abstraction boundary? REAL, IMAG, MAG, ANGLE, and the constructors MAKE-POLAR and MAKE-RECT -------------------------------------------------- (Note: We *could* just implement complex numbers in terms of, say, rectangular coords, and convert to and from polar whenever we need to, but that doesn't let us automatically use whatever is most efficient which we could do with both representations coexisting.) Given that we want both types to co-exist, we now want to define GENERIC operations REAL, IMAG, MAG, and ANGLE which return the appropriate number regardless of which type was given. That is we would like selectors that operate on complex numbers regardless of which form they are in. This can be done by defining generic functions, which are a collection of functions that together share the same name. We could simply implement a function, REAL, that called the appropriate function depending on the type of argument In our table above, REAL corresponds to a row (the top row). (define (real (z )) (cond ((rect? z) (rect-real z)) ((polar? z) (* (polar-mag z) (cos (polar-angle z)))))) This is a DISPATCH ON TYPE, which we saw in the symbolic differentiation lecture, because the type of the argument is used to determine the correct function to call (dispatch to the right thing). And analogously for IMAG, MAG and ANG -------------------------------------------------- Swindle has a built-in mechanism to do type dispatching, known as generic functions. A generic function is a collection of methods, where the MOST APPROPRIATE method is invoked, given the particular type of argument. A generic function is defined using the special form (defgeneric NAME PARAMETERS) which defines a generic, with the given name, where each method of the genric takes the given list of parameters (actually only the number of these parameters matter so their names & types is just left as documentation). Methods (functions) are added to the generic using the special form (defmethod (NAME TYPED-ARGS) ...) where a METHOD is constructed and added to the existing generic named NAME. [Note: defmethod will define a generic first if this wasn't done, so it makes defgeneric completely redundant except for documentation purposes.] The generic function decides which of the specific methods to run based on the types of the arguments passed to the generic, and the types of the formal parameters of the specific methods. The "most appropriate" function, given the types, is found and run. The most appropriate function is defined as the one with the list of formal parameters whose types most closely match the given argument types. In today's lecture this is simple, because there is only one function whose parameter list matches a given argument, because no value can be of both type and . However, in general, with hierarchical types there could be several methods whose types match a given argument list. In such cases, the "more specific" match is used, the type which is closest to the type of value. This will be covered in more detail next time. So for complex numbers: (defgeneric (real (z ))) (defmethod (real (z )) (rect-real z)) (defmethod (real (z )) (* (polar-mag z) (cos (polar-angle z)))) Similarly for MAG, ANG, IMAG Note: a generic cannot have more than one function with the same list of parameter types! Multiple definitions simply replace the previous one. Generic functions do a dispatch on type, but it is done by the system rather than explicitly by the programmer. In a language that does not support generic functions directly, it can be done using COND to do the dispatch explicitly (as above). Now if we define (define z1 (make-rect 1 2)) Then (real z1) = ({real} { 1 2}) = (rect-real { 1 2}) = {1} Aside: Many operations like add, equals, etc. in Swindle are GENERIC operations: --- they operate on many types of data --- They sometimes do TYPE COERCION, converting from one type to another. We will talk about this next time. --- can add new methods to them! -------------------------------------------------- A second way to implement generic operations is by MESSAGE PASSING: represent objects as entities that can accept messages about what operations to perform. Every object knows how to handle various operations. * DISPATCH ON OPERATION (rather than on type) So the data is "active" -- it knows how to compute. e.g., 7 is a number that knows how to add itself to another number, multiply, etc... Swindle does not support this directly, but we can implement it using higher order procedures. In MESSAGE PASSING, each data type knows how to handle the operations. An object is told which operation to perform by sending it a message. One way to implement this is using higher order procedures: (define (make-rect1 (r ) (i )) (lambda (op) (cond ((eq? op 'real-part) r) ((eq? op 'imag-part) i) ((eq? op 'magnitude) (sqrt (+ (square r) (square i)))) ((eq? op 'angle) (atan i r)) ((eq? op 'type) 'rectangular) (else (error "Unknown op for rectangular."))))) make-polar1 is analogous, it also returns a function. These are then used by "passing messages", e.g., (define z (make-rect1 3 4)) (z 'imag-part) ==> 4 Note that in this implementation all the complex numbers are represented as objects of type , because they are implemented as functions. Thus each object explicitly knows its type (in response to the message 'type). -------------------------------------------------- Note that both techniques spend a *lot* on big cond clauses to figure out what to do. When there is direct programming language support, as here is for generic functions in Swindle, then there is no explicit cond clause but in effect it is still there. "Middle managers" We can eliminate this paper-pushing administrative cost by DATA DIRECTED PROGRAMMING: * Make use of the following observation about generic operations: 2D table of type-specific actions indexed by data types and operations. Just put the method's to do the work in the table! COLUMNS: dispatch on operation (message-passing style) ROWS: dispatch on type This is a "Grand Unified View" -- look at the table as a combination of type tag and operation, and forget the details of how the dispatching is done. This style of code is a bit awkward to read, so we won't go into it. Things seem to be converging on the generic functions style, as it is most like conventional functions/procedures. You may run into the message passing style though, as its used in languages like Smalltalk. -------------------------------------------------- Sneak preview: Generic Operations and Coercion. Extended Example: Generic Arithmetic System. Generic operations are a technique for organizing (large) systems. Means of managing complexity through abstraction. Key ideas are: * Define 'abstract methods' that make sense on many types of objects * Hide the type-specific implementation from the user. For example, you can ADD: - exact numbers ("integers") - rationals - inexact numbers ("reals") - complexes - polynomials - etc. but the details of how you add them are quite different. We have seen three ways of implementing generic operations: * Generic Functions (Dispatch on Type) * Message Passing (Dispatch on Operation) * Data Directed (table lookup) The point of this was, > Combination of data type and operation name > tell you which procedure to use. > The three different ways were different ways > of searching a "table of operations". -------------------------------------------------- Concept of the day: Generic Operations Implementable in several ways: * generic functions (dispatching on type) * message passing (dispatching on operation) * data-directed programming (table of functions indexed by type and operation) Swindle supports generic functions directly in the language, with the special form DEFGENERIC and with DEFMETHOD