----------------------------------------------------------------- Today: Type dispatching Manipulating symbolic expressions Example: symbolic differentiation ---------------------------------------------------------------- The basic tool: type dispatching. Suppose we want to write a function that computes something slightly different depending on the type of its argument. EXAMPLE: one-less (x) returns x-1 if x is a , returns the tail of x if x is a , otherwise returns x (define (one-less ) (method ((x )) (cond ((instance? x ) (- x 1)) ((instance? x ) (cdr x)) (else: x)))) This is known as a dispatch on type, because for each type of argument, a different action is taken. We will see next time that Dylan has a built in mechanism for doing type dispatching, known as generic functions, but for today we will explicitly do the type dispatch rather than using the generic function mechanism (just to show you, there is no magic in generic functions). ---------------------------------------------------------------- Symbolic differentiator. Code is on the Web. Rules for taking derivatives: d -- c = 0 dx d -- x = 1 dx d -- v = 0 for v an independent variable dx d du dv -- (u+v) = -- + -- dx dx dx d dv dv -- (u*v) = u* -- +v*-- dx dx dx d n (n-1) du -- (u ) = n (u ) -- for n>0 dx dx We are going to manipulate *symbolic* *expressions* using these rules. For example: d (ax^2 + bx) / dx = 2ax + b ax^2+b^x is a symbolic expression -- it's not a function -- it's a data structure with symbols (a,b,x) in it. Symbolic differentiation is: -- Exact -- Gives formulas as results. Numerical differentiation (which we saw when we did higher-order functions) is: -- Approximate -- Gives numbers as results. We're going to program the six rules above. * This is one of Lisp's original successes: Macsyma and now Mathematica are common tools for scientists. --------------------------------------------------------------------- We are going to restrict ourselves to *binary* operators, just two arguments. (+ A B) (* A B) (^ A B) -- exponentiation. So, we'll write `a+b+c' as `(+ a (+ b c))'. No loss of power. We will represent expressions in the usual Dylan prefix style. 2ax ==> (* 2 (* a x)) ax^2+bx ==> (+ (* a (^ x 2)) (* b x)) [Maybe draw an expression tree for this?] --------------------------------------------------------------------- Our Data Abstraction: We're going to have the following TYPES of expressions: [CONSTANTS]: Type: Predicate: constant? >> represented concretely as s -- this costs us flexibility, no abstraction, but means we can directly use Dylan arithmetic ops [VARIABLES]: Type: Predicate: variable? (same-variable? v1 v2) [SUM]: Type: Predicate: sum? Accessors: addend, augend Constructor: make-sum Contract: If x = (make-sum a b) then (addend x) + (augend x) = a+b Note: this contract allows sums to be manipulated as long as maintain mathematical equality to summation. [PRODUCT]: (* a b) Type: Predicate: product? Accessors: multiplier, multiplicand Constructor: make-prod Contract: If x = (make-prod a b) then (multiplier x) * (multiplicand x) = a*b [EXPONENT]: (^ a b) Type: Predicate: exponent? Accessors: base, power Constructor: make-expt Contract: If x = (make-expt a b) then (base x) ^ (power x) = a^b --------------------------------------------------------------------- The derivative function takes: > Expression to be differentiated > Variable to differentiate wrt. (dd '(+ x 3) 'x) means d (x+3) ------- dx DD converts from list form into the correct types of expressions (just defined above) and then call deriv. Deriv does a DISPATCH on the TYPE of expression * Dispatch to the code that does the right thing for each specific type of expression, constant, variable, sum, product, expt. (define (deriv ) (method ((e ) (v )) (cond ((constant? e) 0) ((variable? e) (if (same-variable? e v) 1 0)) ((product? e) (make-sum (make-prod (multiplier e) (deriv (multiplicand e) v)) (make-prod (deriv (multiplier e) v) (multiplicand e)))) ((sum? e) (make-sum (deriv (addend e) v) (deriv (augend e) v))) ((exponent? e) (make-prod (make-prod (power e) (make-expt (base e) (make-sum (power e) (make-const -1)))) (deriv (base e) v)))))) --------------------------------------------------------------------- Take a look at how one of the underlying types is implemented (define-class () (addend ) (augend )) (define (make-sum ) (method ((a1 ) (a2 )) (make addend: a1 augend: a2))) (define (sum? ) (method ((e )) (instance? e ))) (define (addend ) (method ((s )) (get-slot addend: s))) (define (augend ) (method ((s )) (get-slot augend: s))) New special form define-class, three new procedures, make, instance? and get-slot (define-class NAME SUPERS slot1 ... slotn) defines a new CLASS where sloti is (SLOTNAME TYPE [INITVAL]) Note: SUPERS is for now always (), used for building a type hierarchy (make CLASS [KEY1: VAL1 ... KEYN: VALN]) creates an INSTANCE of the named CLASS (note, must be user defined class) The keywords optionally specify slot names and the initial values to use for each named slot. (instance? VAL TYPE) tests whether VAL is an instance of the given TYPE (get-slot SLOT: VAL) returns the value of the named SLOT: for the given instance --------------------------------------------------------------------- The moment of truth is... try it out! (dd '(* x 2) 'x) ==> (+ (* x 0) (* 1 2)) Ugh. Gross. But correct at least. We want to *simplify* the result, to get just 2 as an answer. To do this, we just change the constructors to do arithmetic simplification: (make-sum a b): - If a or b is zero just return the other one - If a and b are both numbers then add them as numbers. (make-product a b) - If a=0 or b=0 then 0 - If a=1 or b=1 then return the other. - If both are numbers, multiply them by hand. (make-expt a b) - If b=0 then 1 - If b=1 then a Note: that these changes still meet the above contracts, because they allow any manipulation that preserves equality to the original sum, product or expt. Let's take a look at the code for the changed make-sum: (define (make-sum ) (method ((a1 ) (a2 )) (cond ((and (constant? a1) (constant? a2)) (+ a1 a2)) ((= a1 0) a2) ((= a2 0) a1) (else: (make addend: a1 augend: a2))))) Now, we get: (dd '(* x 2) 'x) ==> 2 (dd '(+ (* a (^ x 2)) (* b x)) 'x) ==> (+ (* a (* 2 x)) b) (dd .... 'a) ==> (^ x 2) This is amazingly little code for computing derivatives! --------------------------------------------------------------------- Suppose we want to get ``better'' output, using infix form: (a + b) instead of (+ a b) Trick: Just change the routines that convert from printed format to internal Change the representation so that the operator is in the middle! Changing a few lines of code will do it... The "printed" and "internal" representations are completely distinct. In (define (erep ) (method ((p )) (cond ((or (instance? p ) (instance? p )) p) ((= (head p) '+) (make-sum (erep (head (tail p))) (erep (head (tail (tail p)))))) ((= (head p) '*) (make-prod (erep (head (tail p))) (erep (head (tail (tail p)))))) ((= (head p) '^) (make-expt (erep (head (tail p))) (erep (head (tail (tail p))))))))) Change (head p) to (head (tail p)) and (head (tail p)) to (head p) Then, (dd '((a * (x ^ 2)) + (b * x)) 'x) ==> ((a * (2 * x)) + b) That's it! deriv itself doesn't need to change, one bit! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Notice that we never said that (make-sum A B) had to be of type sum; we just insisted that the data structure (make-sum A B) corresponds to the mathematical sum of A and B. This allows, e.g. (make-sum 'x 'x) to return (* 2 x) because they both correspond to the polynomial 2x. (* 2 x) is a product, not a sum at all. In general, it allows all our simplifications. (Analogous to reducing fractions to lowest terms.) --------------------------------------------------------------------- Now, let's look at proving that this procedure works correctly: SHOW: (deriv e v) = de/dv by induction on ... depth of expressions. BASE CASE: depth of e is zero * e is a number (deriv e v) = 0 = de/dv * e is a variable * e is v (deriv e v) = 1 = de/dv * e is some other variable, (deriv e v) = 0 = de/dv INDUCTION CASE: ASSUME (deriv e v) = de/dv for any expression e of depth <= n. SHOW (deriv f v) = df/dv for f of depth n+1 There are three cases (1) f is the sum of f1 and f2 (deriv f v) = (make-sum (deriv (addend f) v) (deriv (augend f) v)) <<< substitution = (make-sum (deriv f1 v) <<< Contract (deriv f2 v)) = (make-sum df1/dv df2/dv) <<< by IH = df1/dv + df2/dv <<< by rules for sums = df/dv (2) f is the product of f1 and f2 (deriv f v) = (make-sum (make-prod f1 (deriv f2 v)) (make-prod f2 (deriv f1 v))) = (make-sum (make-prod f1 df2/dv) (make-prod f2 df1/dv)) = f1 df2/dv + f2 df1/dv = df/dv (3) f is f1 to the f2, where f2 is a number >= 0 (deriv f v) = (make-prod (make-prod f2 (make-expt f1 (- f2 1))) (deriv f1 v)) = f2 * (f1^(f2-1)) * df1/dv = df/dv --------------------------------------------------------------------- We've used relatively simple structures: classes with two slots. And mostly very simple procedures To perform fairly complex tasks (differentiation). Structuring the problem was very important: * Good abstractions * Good decomposition >> Notice how easy it was to make fairly major changes - add simplification - Hack representation to print in infix. >> It might have looked like the bottom layer of the abstraction was a lot of extra work for not much benefit. - We knew what the rep was - Most of the functions were trivial. >> But, having that layer of abstraction let us make some drastic changes underneath it without changing the things built on top of it. It'd be quite easy to write five times as much code that does the same thing.