Object Oriented Programming The way you do something depends on the object you are doing it to. E.g., sorting of numbers versus names versus lists, versus strings computing area of a figure. Suppose we want to define a number of geometric shapes and write a function to compute their area. How would you do it in standard lisp? We first define: (defstruct triangle (base 0) (altitude 0)) (defstruct rectangle (breadth 0) (width 0)) (defstruct circle (radius 0)) And then write a function to compute area of all these figures: (defun area (figure) (cond ((triangle-p figure) (/ (* (triangle-base figure) (triangle-altitude figure)) 2)) ((rectangle-p figure) (* (rectangle-breadth figure) (rectangle-width figure))) )) This is too complex and unreadable. One way to put more structure into this is through procedural abstraction. We can, for example, write a separate function to compute the area of each figure and call them appropriately. (defun area (figure) (cond ((triangle-p figure) (triangle-area figure)) ((rectangle-p figure) (rectangle-area figure)) ((circle-p figure) (circle-area figure)) )) (defun triangle-area (figure) (/ (* (triangle-base figure) (triangle-altitude figure)) 2)) (defun rectangle-area (figure) (* (rectangle-breadth figure) (rectangle-width figure))) (defun circle-area (figure) (* *pi* (circle-radius figure) (circle-radius figure))) But this is also cumbersome. What we need is an automatic mechanism that dispatches the arguments to appropriate procedures, with the following advantages: 1. Automatic association of procedures (methods) with data types (classes). 2. Easier to maintain -- add and delete object types (classes) (e.g., add squares to the above hierarchy) 3. Improved readability. So in the new approach, we define the three different shapes as three classes. We then write a method for each class. (defmethod area ((figure triangle)) (/ (* (triangle-base figure) (triangle-altitude figure)) 2) (defmethod area ((figure rectangle)) (* (rectangle-breadth figure) (rectangle-width figure))) (defmethod area ((figure circle)) (* *pi* (circle-radius figure) (circle-radius figure))) There is no need to write the top-level area function any more! The system automatically uses the appropriate method on each object depending on its class! Common Lisp Object System (CLOS) CLOS is an object oriented subset of Common Lisp. Object - oriented Programming centers around data or "objects". The objects belong to classes. Each class has some "methods" or procedures associated with it. Examples -------- * Objects are blocks, wedges or spheres. Methods are clear, move, put-on etc. * Objects are computer files, methods are delete, create, copy, rename, etc. * Objects are figures, and methods are cut, paste, resize, move etc. CLOS provides an easy way to define classes and methods. Classes are organized into a hierarchy. Animals ^ ^ ^ ^ | | | | bacteria birds reptiles mammals ^ ^ ^ ^ ^ ^ ^ | | | | | | | Ameba hydra flight flying snakes cats people less An example hierarchy A direct superclass is one which is connected directly to a class in the upward direction. A direct subclass is one which is connected directly to a class in the downward direction. So mammals is a direct superclass of cats and is a direct subclass of animals in the above hierarchy. The words superclass and subclass refer to the transitive closure on direct superclass and direct subclass respectively. Every member of a class is also a member of any class above it to which it is connected - i.e its superclass. CLASSES are defined using defclass (defclass ( :accessor :initform :initarg ) ( :accessor :initform :initarg ) ) Consider the following hierarchy: Person | -------------------------------------- ^ ^ ^ | | | Male child Female ^ ^ ^ | | | ------- ------- ----------- ^ ^ / ^ / ^ | | / | / | | man boy girl woman The upward arrows indicate direct superclasses. Direct subclasses are inverses of these. Superclasses and subclasses are classes which are connected to a given class with upward or downward paths. Notice that our hierarchy is not a tree because some nodes have multiple parents. Nodes like boy and girl, which have more than one parent are called splits. Nodes like person which can be reached in more than one way from a split through upward arrows are called joins. (defclass person ( ) ; | | ; \/ \/ ; class name superclasses ((name :accessor person-name :initarg :name) ; | | | ; \/ \/ \/ ; slot reader function keyword for input (age :accessor person-age ; | | ; \/ \/ ; slot reader function :initform 30 :initarg :age) ; | | ; \/ \/ ; default value input keyword (sex :accessor person-sex :initarg :sex))) ; | | | ; \/ \/ \/ ; slot reader function keyword for input We can create the above hierarchy using the following defclass statements. (defclass male (person) ((age :accessor male-age :initform 25 :initarg :age) (sex :initform 'MALE))) (defclass female (person) ((age :accessor female-age :initform 35) (sex :initform 'FEMALE))) (defclass child (person) ((age :initform 10))) (defclass man (male) ((age :initform 29))) (defclass woman (female) ((age :initform 39))) (defclass boy (child male) ((class :accessor boy-class :initarg :class))) (defclass girl (child female) ((class :accessor girl-class :initarg :class))) The above definitions create the following hierarchy with the slots and default values shown next to the class. name: NIL Person age: 30 | sex: NIL -------------------------------------- ^ ^ ^ | age:25 | | age:35 Male sex:male child age:10 Female sex:female ^ ^ ^ | | | ------- ------- ----------- ^ ^ / ^ / ^ | | / | / | | man boy girl woman age:29 class:NIL class:NIL age:39 Now what can we do? We can think of the classes and methods as a data abstraction, which is what it is. Hence we need to define constructors, readers and writers. ** Create instances - (constructors) > (setf X (make-instance 'man :name '(John Doe) :age 59)) # > X # ** Retrieve slot values How do we retrieve the man's age? (readers) > (person-age X) 59 <----- the default value is over-ridden!! > (person-sex X) MALE <------ the slot values are "inherited" from superclasses. ** Change slot values using SETF (writers) > (SETF (person-sex X ) 'FEMALE) FEMALE > (person-sex X) FEMALE Notice that X is still a man whose sex is "FEMALE"!! Wait a minute! Since JOHN is also a male (because he is a man) why isn't his sex "MALE"? Because the slot values are inherited from the MOST SPECIFIC SUPERCLASS - or the instance itself if it is present. In general, the slot values of objects is determined by a fairly complex hierarchy walking procedure. The above is the simplest rule of this procedure. The rules will be described in more detail in the next lecture. The Rules of Inheritance Values are inherited by classes from superclasses according to the "hierarchy walking procedure." We use the class hierarchy that we defined in the last lecture to illustrate the examples of this lecture. name: NIL Person age: 30 | sex: NIL --------------------------------- ^ ^ ^ | age:25 | | age:35 Male sex:male child age:10 Female sex:female ^ ^ ^ | | | ------- ------- ----------- ^ ^ / ^ / ^ | | / | / | man boy girl woman age:29 class:NIL class:NIL age:39 Rule1: Depth-first rule: ------------------------- Whenever possible the hierarchy-walking procedure WALKS UPWARD, away from the argument class, adding classes to a "class precedence" list as it goes. The order of classes in the precedence list determines which slot values defaults take precedence. > (setf X (make-instance 'MAN)) # > (person -age X) 29 > (setf y (make-instance 'MALE)) # > (person-age Y) 25 > (male-age Y) 25 NOTE: person-age Y and male-age Y both refer to the same slot, although they have different names !! So changing one effects the other! > (setf (person-age Y) (1+ (person-age y))) 26 > (male-age y) 26 >(setf z (make-instance 'person)) # >(person-age Z) 30 But what if a class has more than one superclass? They are called SPLITS. E.g, Boys are both males and children. Rule2: Left-to-Right Rule ------------------------- Whenever a split is encountered explore the hierarchies above the split in left to right order in the defclass statement of split. > (SETF a (make-instance 'BOY)) # > (person-age a) 10 > (SETF b (make-instance 'GIRL)) # >(person-age b) 10 If we had defined boy as (Male child) then the default age would have been 25 !! But what happens if we ask (person-sex a)?? ___ Since a is a boy and boy has no sex slot it has to be inherited from a superclass. ---- Since child is boy's first superclass it goes there first. But child has no sex slot either. ----- Now what? Depth first rule says go to child's superclass -i.e. person. Should it go there and return NIL? Rule 3: Up-to-Join Rule: ----------------------- A class that can be reached in more than one way from a split is called JOIN. When a join is encountered the hierarchy walking procedure stops and backtracks to the latest choice point without looking at the join. It then proceeds with the next choice according to the Left-to-Right rule. If all possibilities to backtrack are exhausted, then it looks at the join and proceeds further. > (person-sex a) MALE So it looks at BOY-CHILD-MALE-PERSON in that order! Similarly > (person-sex b) FEMALE Top Top Top | | | Z L2 M2 R2 J | | | | / | \ Y L1 M1 R1 L2 M2 R2 | \ | / | | | X \ S / L1 M1 R1 | | \ | / Bottom Bottom S | Bottom Figure (1) Figure (2) Figure (3) For Figure (1), the precedence ordering is Bottom-X-Y-Z-Top. For Figure (2), it is Bottom-L1-L2-S-M1-M2-Top-R1-R2. For Figure (3), it is Bottom-S-L1-L2-M1-M2-R1-R2-J-Top. What else can we do? We can associate procedures (methods) with each of the classes or, more precisely - tuples of classes! To illustrate this, let us enhance our definitions a little bit. In particular, we add a slot called wife to each male and a slot called husband to each female. (defclass male (person) ((age :accessor male-age :initform 25 :initarg :age) (wife :accessor wife :initarg :wife) (sex :initform 'MALE))) (defclass female (person) ((age :accessor female-age :initform 35) (husband :accessor husband :initarg :husband) (sex :initform 'FEMALE))) We also add slots called father and mother to the class child. (defclass child (person) ((age :initform 10) (father :accessor father :initarg :father) (mother :accessor mother :initarg :mother))) Now let us consider writing a method called marriage. Marriage should take two arguments, a male and a female, and gets them married by setting their wife and husband slots appropriately. (defmethod marriage ((m male) (f female)) (setf (wife m) f) (setf (husband f) m) 'DONE) By specifying the classes of the arguments in defmethod, we are restricting its access to those classes and subclasses! The above method is only applicable when the first argument is male and the second is female. > (setf p (make-instance 'man :name '(John Doe))) # > (setf q (make-instance 'woman :name '(Mary Rose))) # > (marriage p q) DONE >(person-name (wife p)) (MARY ROSE) >(person-name (husband q)) (JOHN DOE) Unfortunately, our "marriage" does not prohibit child marriages! > a # > b # > (marriage a b) DONE Since a boy is a male and a girl is a female, the method applies to them as well! IMPORTANT: Methods are inherited the same way that slot values are! I.e., the same rules of the hierarchy walking procedure apply. So in order to prevent child marriages, we can associate a special method "marriage" to the class tuple . (defmethod marriage ((x child) (y child)) (send-messages 'authorities (father x) (mother x) (father y) (mother y))) We also have special methods when one of the arguments to marriage is an adult. (defmethod marriage ((x male) (y child)) (arrest x)) (defmethod marriage ((x child) (y female)) (arrest y)) Now what method will be used when we call marriage with a boy and a girl as arguments? How does the hierarchy walking procedure work when a method has more than one argument? 1. A method is an "applicable method" if the arguments are instances of the classes defined in the defmethod's parameter list. 2. Using the class hierarchy over the first argument, the hierarchy walking procedure establishes the precedence ordering among applicable methods. 3. If there is a unique method which is preferred at this point, it is used. If there are two or more methods that have the same precedence according to the first argument, the selection algorithm uses the other arguments in left to right order to select one of those methods. Let us denote a method by a tuple of its argument classes, e.g., denotes the method which is defined with (defmethod marriage ((m male) (f female)) .....) What happens when a and b are children and (marriage a b) is called? 1. Applicable methods (a) (b) (c) (d) 2. Use the hierarchy over the first argument to filter the methods. By the left-to-right rule child takes precedence over male. So we have (b) (c) 3. Use the hierarchy over the second argument to filter the remaining methods. Once again, by the left-to-right rule, child takes precedence over female. So -- (b) would be picked when a and b are children. The following table assumes that: m is man w is a woman b is a boy and g is a girl. ------------------------------------------------------------------------------- Arguments to Applicable methods After first argument After second argument Marriage filtering filtering ------------------------------------------------------------------------------- (m w) ------------------------------------------------------------------------------- (b g) (by Left-to-Right rule) ------------------------------------------------------------------------------- (b w) (by Left-to-Right rule) ------------------------------------------------------------------------------- (m g) (by Left-to-Right rule) ------------------------------------------------------------------------------- Auxilary Methods ---------------- So far, what we have seen is how a primary method is selected. In addition to the primary methods, one can also have :before and :after methods. These are defined using the keywords :before or :after, after the name of the generic function in the defmethod statement. For example, (defmethod marriage :before ((m male) (f female)) (send-invitations ..... )) (defmethod marriage :after ((m male) (f female)) (pay-up-the-bills .... )) Unlike the primary method, ALL the applicable :before methods are executed in the precedence order previously defined (using the 3 rules of inheritance) before the one selected primary method. After the primary method, all :after methods are executed, again in the precedence order defined by the rules of inheritance. We can also have :around methods, which are executed instead of the primary method. If an applicable :around method exist for the given arguments, it is chosen according to the precedence relationships. It can optionally call the primary method using the function (call-next-method). Even a primary method can call the next applicable primary method using the function (call-next-method) (whether or not there are any around methods). Operator Method Combination -------------------------- The above way of combining methods is called "Standard Method Combination". CLOS also provides what is called "Operator Method Combination". In this way of combining methods, ALL applicable primary methods are executed in the precedence ordering, and the results are combined using an operator. The operator can be one of: +, and, append, list, max, min, nconc, or, progn. We have to specify a single such operator for a generic function using a defgeneric statement. It looks as follows. (defgeneric marriage (m f) (:method-combination progn)) Now, EACH method of the above generic function should declare progn as its method combinator. (defmethod marriage progn ((m male) (f female)) ....) (defmethod marriage progn ((c child) (d child)) ....) and so on. This will have the effect that all applicable primary methods will be executed for any objects in the precedence order in sequence and the value returned by the last such method is returned (because progn is the method combinator). If we used list instead of progn, for example, a list of all the results of all applicable primary methods would have been returned. A couple of tidbits: 1. Methods can be specialized to individual instances. > (setf x (make-instance 'man)) # > (defmethod marriage ((m (eql x)) (f female)) (inform-me)) #) FEMALE) @ #x115223e> The above method applies only when the first argument is that particular individual x. This is particularly useful in debugging. You can take a buggy method, specialize it to individual examples and track down the bugs. 2. Objects can make circular references without serious problems. For example, if men and women have spouse slots, a man's spouse slot could point to a woman, and the woman's slot could point to the man. There is no problem with this because only pointers are stored as the slot values. An object's printed representation does not contain the spouse's printed representation. In fact it does not contain any of the slot values. So there is no danger of trying to change circular pointers and getting into an infinite loop! Compare this situation to one of creating a circular list. For example, let X be set to the list (A B C). >(setf x '(a b c)) (A B C) What happens if you do (setf (rest x) x)? The rest pointer of x now points to x itself. So there is a circular list! There is no danger as long as the system is not required to print it. But if the system has to print it, it could go into an infinite loop in some implementations. Some systems are more clever in avoiding this trap! For example, in franz you get the following output: >(setf (rest x) x) (A A A A A A A A A A ...) Usually objects have their names stored as the values of some slots, called id or name. This name can then be used to access the object itself. The circularity does not pose a problem. Object symbol table ______________________ __________ | |_________|__ | ->___________ | | | |ID: ob1 ------------------->| pointer| | | | | | | | | | | | | |_________| | | | | |________| > (setf john (make-instance 'man :name 'john)) # > john # >(person-name john) JOHN >(person-name (person-name john)) Error: No methods applicable for generic function # with args (JOHN) of classes (SYMBOL) [condition type: PROGRAM-ERROR] >(person-name (eval (person-name john))) JOHN Summary - Object-oriented programming makes it easy to write procedures that are specialized to the particular classes of objects. - Classes enable inheritance of slot-values and methods from superclasses. - The rules of the hierarchy walking procedure -- the depth-first rule, the left-to-right rule, and the up-to-join rule -- determine which method will be used in a given situation. - Argument order determines the precedence when the methods have more than one argument. The methods are filtered using the hierarchies over the argument classes in left to right order. - Auxilary methods can be used before, after, or instead of the primary method. They are declared using the keywords :before, :after, and :around. - In operator method combination, the results of executing all the primary methods are combined using an operator. - Object oriented programming is useful when the program can be naturally decomposed into methods, each of which is specialized to a set of classes.