TEACH OBJECTCLASS_EXAMPLE Aaron Sloman 11 April 1992 (Based partly on TEACH FLAVOURS and HELP OBJECTCLASS) This gives some illustrative examples of the use of LIB OBJECTCLASS, which is described in more detail in HELP * OBJECTCLASS. SEE LIB * OBJCLASS_EXAMPLE for the full listing of the example code discussed in this tutorial. CONTENTS - (Use g to access sections) -- Getting started -- Introduction -- Object Oriented Programming -- Some Syntax -- Example: defining objectclass person -- Procedures created automatically by define :class -- Four ways of creating instances -- -- (1) Using consperson -- -- (2) Using newperson -- -- (3) Using instance ... endinstance -- -- (4) Using define :instance -- Slots, or instance variables, and the field accessors -- Recogniser procedures -- Defining Methods -- Defining subclass adult of person -- Circular Structures and Printing -- The marry method -- A slightly different definition of "marry" -- Improving the printing of adults -- Inheritance -- The professor objectclass -- Multiple Inheritance -- The Ordering of Components -- Daemons -- Example using call_next_method -- Updater Methods -- Wrappers -- Computing slot defaults at compile time -- Methods can have untyped arguments -- Metaclasses -- Bugs and Omissions -- See Also -- Getting started ---------------------------------------------------- To make the package described in this teach file available you need to do lib objectclass or uses objectclass After that you will be able to run the examples given below, e.g. using the "mark and load" facilities described in TEACH *MARK and TEACH *LMR. -- Introduction -------------------------------------------------------- For an introduction to the OBJECTCLASS package and a discussion of some of its features see HELP * OBJECTCLASS. Compare TEACH * FLAVOURS if you wish to find out about the FLAVOURS package. This file is partly modelled on it to provide a basis for comparison. -- Object Oriented Programming ----------------------------------------- TEACH FLAVOURS gives an introduction to object oriented programming and some of the key concepts involved. -- Some Syntax --------------------------------------------------------- In the Objectclass package, new classes are introduced using the define-syntax with the keyword ":class" after "define". The syntax is broadly modelled on that of recordclass, since objects are records. A simplified schema for defining a new object class is define :class ; is ; slot = ; slot = ; ... enddefine; Methods that perform actions on instances of that class are defined using the syntax define :method (:); ...actions... enddefine; Examples of objectclass definitions and method definitions are given below. The full range of options that can be used is explained in the files REF * OBJECTCLASS * DEFINE_CLASS * DEFINE_METHOD. -- Example: defining objectclass person ------------------------------- We are going to start by defining a class called "person", which will represent people whose age, name and sex interests us. Later we shall define subclasses of people with additional features of interest, including adults (who are capable of having spouses) and professors, (who have subjects, and who sometimes retire). Here is a simple definition for an objectclass to represent people. We assume each person has a name, an age and a sex. And we specify default values for these attributes. define :class person; slot person_name = undef; slot person_age = 0; slot person_sex = undef; enddefine; STYLISTIC WARNING: if you define an objectclass like this with short attribute names, e.g. "name" instead of "person_name" and "age" instead of "person_age" then you are likely later on to forget that you must not use the same identifier for a different purpose. Thus for safety start all attribute names with a reminder of the class to which they are relevant. This will be more verbose, but less likely to lead to errors. (Using sections could reduce the risk of error due to name clashes. See HELP * SECTIONS) -- Procedures created automatically by define :class ------------ Like recordclass, the :class declaration creates a lot of procedures. In fact, it creates a lot of the same procedures and gives them the same names as recordclass does. See HELP * RECORDCLASS. (The "defclass" construct is more general, but as it was added to the language later, more Pop-11 users may be familiar with recordclass. See HELP * DEFCLASS for details.) Firstly, :class creates a collection of constructors and destructors. If you've used recordclass then you'll have expected "consperson" and "destperson". The contructors are described in more detail in the next section on creating instances. vars P, Pname, Page, Psex; consperson("fred", 5, "male" ) -> P; ;;; creates person P P => ** destperson( P ) -> (Pname, Page, Psex); ;;; extracts the person_, age and sex from a person instance Pname => ** fred destperson(P) => ** fred 5 male -- Four ways of creating instances ------------------------------------ There are four ways of creating an instance of an objectclass, using the cons... and new... procedures corresponding to the objectclass, and the instance ... endinstance define :instance ... enddefine syntax forms described below. -- -- (1) Using consperson The cons... procedure, is analogous to the constructor produced by recordclass, and must be given the values to be assigned to slots. For example: vars adam = consperson("adam", 24, "male"); adam => ** isperson(adam) => ** -- -- (2) Using newperson newperson() -> P; ;;; constructs a new default person P => ** -- -- (3) Using instance ... endinstance If you want to create a male person aged 77, and don't yet know the name you can do this: vars ppp; instance person; person_age = 77; person_sex = "male" endinstance -> ppp; ppp=> ** -- -- (4) Using define :instance A minor variation on the -instance- syntax is the define:instance syntax. This allows you to invent an instance and bind it to a variable at the same time. define :instance xxx:person; person_age = 77; person_sex = "male" enddefine; xxx => The -instance- syntax is included to help make programs creating instances with lots of non-default slots more attractive. A detailed description of the -instance- syntax is provided in REF * INSTANCE * DEFINE_INSTANCE. -- Slots, or instance variables, and the field accessors -------------- "define :class" creates three slots or instance variables. In this documentation we shall call them slots or fields but most other OOPS (including the FLAVOURS documentation) call them "instance variables", following the names establish by SmallTalk. The objectclass slots have corresponding accessing procedures, with the same names. newperson() -> P; P => ** person_name( P ) => ** undef "mary" -> person_name( P ); person_name( P ) => ** mary 16 -> person_age( P ); person_age( P ) => ** 16 "female" -> person_sex( P ); person_sex( P ) => ** female P => In other object-oriented packages you might need to send a message "name" to P in order to get the name, e.g., in FLAVOURS P <- person_name -> x; But this will not work with the objectclass package. Objectclass simply uses procedures instead of messages. The procedures associated with an objectclass are often referred to as "methods". Thus there is a method (with an updater method) associated with each slot or field in an objectclass. -- Recogniser procedures ---------------------------------------------- "define :class" also creates recogniser procedures. "isperson" is a recogniser that returns true for persons and for any sub-classes. isperson( P ) -> bool recognises persons and sub-classes isperson(P) => ** isperson(99)=> ** -- Defining Methods --------------------------------------------------- In order to perform operations on instances of objectclasses, users can define additional methods (i.e. additional to the ones created automatically). Objectclass methods are just procedures that act differently depending on the types (keys) of their arguments. Although there is complete freedom to define methods to do completely different things to different classes of objects (e.g. persons and trees) it is a good idea to try to make them at least roughly analogous. Compare the way the Pop-11 operator "<>" can be applied to words, lists, strings, procedures, etc. Different definitions of a method are needed for each combination of objectclasses. For example, you could write a method for giving a person a birthday. define :method birthday( p:person ); lvars p, age; person_age(p) + 1 -> age; age -> person_age(p); [Happy birthday ^(person_name(p)) - now aged ^age] => enddefine; vars joe = consperson("joe", 0, "female"); joe => ** birthday(joe); ** [Happy birthday joe - now aged 1] person_age(joe) => ** 1 birthday(joe); ** [Happy birthday joe - now aged 2] HELP OBJECTCLASS gives additional examples of method definitions. -- Defining subclass adult of person ---------------------------------- When people grow up they may acquire a spouse. So we define a new subclass of persons, called adults, who are able to have a spouse. But the spouse may be nonexistent, which we'll make the default. define :class adult; is person; ;;; specify superclass slot person_spouse = false; ;;; and additional slot enddefine; Because of the second line, the adult objectclass is a subclass of the person objectclass. However, every instance of adult is also an instance of person, though not vice versa. For example define :instance adam:adult; person_name = "adam"; person_age = 33; person_sex = "male"; enddefine; define :instance eve:adult; person_name = "eve"; person_age = 35; person_sex = "female"; enddefine; define :instance dot:adult; person_name = "dot"; person_age = 25; person_sex = "female"; enddefine; Try these print commands adam => dot=> The printing is rather verbose. We'll see below how to limit it. -- Circular Structures and Printing ----------------------------------- We shall shortly define a method for linking two adults in marriage, but before doing that we need to do something about the fact that if two individuals are spouses of each other then the print routine might get stuck in a loop. Fortunately, the default printing routine checks for recursive loops. Just to check that it does not get stuck in printing loops, we can do this: vars a1 = instance adult person_name = "a1" endinstance, a2 = instance adult person_name = "a2" endinstance; a1 -> person_spouse(a2); a2 -> person_spouse(a1); Now there's a loop as a1 is the spouse of a2, and a2 is the spouse of a1. Will the printing get stuck in a loop? a1 => ** )>> a2 => ** )>> It's rather verbose. Later we'll offer a better method for printing adults. Now we can define the marry method. -- The marry method --------------------------------------------------- First we define a couple of subroutines to check legality and perform the actions we need. define check_bigamy(p1, p2); lvars p1, p2, spouse; person_spouse(p1) -> spouse; if spouse then ;;; check p1 is not already married to someone else unless spouse == p2 then mishap('BIGAMY', [% p1, p2 %]); endunless endif enddefine; define take_spouse(p1, p2); lvars p1, p2; ;;; update my spouse slot p2 -> person_spouse(p1); ;;; take the vows [I ^(person_name(p1)) take thee ^(person_name(p2)) to be my lawfully wedded other] => enddefine; define :method marry( p1:person, p2:person ); lvars p1, p2, spouse1, spouse2; ;;; see if p1 and p2 are of the same sex if person_sex(p1) == person_sex(p2) then [hmm very modern] => endif; check_bigamy(p1, p2); check_bigamy(p2, p1); take_spouse(p1, p2); take_spouse(p2, p1); enddefine; Having defined the method we can now use it: marry(adam, eve); ** [I adam take thee eve to be my lawfully wedded other] ** [I eve take thee adam to be my lawfully wedded other] person_name(person_spouse(adam)) => ** eve person_name(person_spouse(eve)) => ** adam What if adam tries to marry dot? marry(adam, dot); ;;; MISHAP - BIGAMY ;;; INVOLVING: '''>' ''>' ;;; DOING : sysprmishap mishap check_bigamy .... Is adam the spouse of his spouse? person_spouse(person_spouse(adam)) == adam => ** -- A slightly different definition of "marry" ------------------------- In an object oriented system that uses message sending, a different style of definition of marry would be more natural. It is illustrated by the marry method shown in TEACH * FLAVOURS/marry as part of the definition of flavour person. A close translation of that approach using objectclass would be the following. define :method marry( p1:person, p2:person ); lvars p1, p2, spouse; ;;; see if p1 and p2 are of the same sex if person_sex(p1) == person_sex(p2) then [hmm very modern] => endif; person_spouse(p1) -> spouse; if spouse then ;;; check p1 is not already married to someone else unless spouse == p2 then mishap('BIGAMY', [% p1, p2 %]); endunless else ;;; not already married. Fix it ;;; update my spouse slot p2 -> person_spouse(p1); ;;; take the vows [I ^(person_name(p1)) take thee ^(person_name(p2)) to be my lawfully wedded other] => ;;; Now make sure the other person does the same marry(p2, p1) endif enddefine; If you try using this method to marry two people of the same sex it prints out an extra line. Why? Because it calls itself recursively whereas the first version doesn't. This makes the first version a bit more efficient. The second definition of method marry could add a test before starting the recursive call. Apart from that the two versions are functionally equivalent. -- Improving the printing of adults ---------------------------------- We can improve the printing of adults by having only the NAME of the spouse printed out. define :method print_instance(a:adult); lvars a, spouse; person_spouse(a) -> spouse; printf( '', [% person_name(a), person_age(a), person_sex(a), if spouse then person_name(spouse) else false endif %]) enddefine; (See HELP * PRINTF if necessary). Now test it adam => ** -- Inheritance -------------------------------------------------------- See TEACH * INHERITANCE for more details. The objectclass adult inherited the person_age, person_name and person_sex slots from objectclass person, and added a new slot of its own, person_spouse. We can now define new objectclass professor, a subclass of adult, to inherit slots and methods from adult and person. We could make an objectclass definition with slots such as name, age, sex, spouse, telephone_number, discipline and then copy the methods we have already defined for the person objectclass and add the new ones for the professor-specific actions. This would be a rather painful way of going about things. It would also mean that if we were to want to change the actions of the marriage method, we would have to change it both for the adult objectclass and for the professor objectclass (and any other classes that we have defined for special classes of adult). What we want to say is that professors are special kinds of people that have all the attributes of adults (and perhaps some others as well) and are capable of being dealt with by all the methods that apply to persons and adults as well as perhaps some special methods specific to professors. The terminology for this is that the professor objectclass "inherits" from the adult and person objectclasses. We then say that "adult" and "person" are superclasses of "professor". This process of defining a new subclass of a pre-existing class is called "specialisation". -- The professor objectclass ------------------------------------------ We are now ready to define a professor as an adult with additional attributes. define :class professor; is adult; ;;; superclass slot telephone_number; ;;; two new slots slot discipline; enddefine; This time we have not specified the default values for the new slots, so let's see what the default is: vars roger = newprofessor(); "penrose" -> person_name(roger); telephone_number(roger) => ** undef discipline(roger) => ** undef If a slot value is unspecified it defaults to -undef-. (See TEACH *SLOT_DEFAULTS for exact information of defaulting.) Let's see how a professor is printed out: roger => ** > That's a bit clumsy, so we can define a special printing method for professors. define :method print_instance( p:professor ); lvars p; pr(''); enddefine; Who cares about the age, sex and spouse of a professor? We can test this on an example: roger => ** Additional methods specific to professors are possible. First lets introduce a new object class for subjects on which professors can write: define :class subject; slot subject_name = 'undecided'; enddefine; Here's how a professor writes a paper on a subject. define :method write_paper(p:professor, s:subject); lvars p, s; [^(person_name(p)) is writing a paper on ^(subject_name(s))] => /* now insert code for a professor to write a paper */ enddefine; Here's how a professor writes a paper on another professor. define :method write_paper(p:professor, s:professor); lvars p, s; [^(person_name(p)) is writing a paper criticising ^(person_name(s))] => /* now insert code for a professor to write a paper */ enddefine; vars rel = instance subject; subject_name = "relativity" endinstance; rel => ** write_paper(roger, rel); ** [penrose is writing a paper on relativity] Now get him to write about a professor called jones: write_paper(roger, instance professor; person_name = "jones" endinstance ); ** [penrose is writing a paper criticising jones] So a professor has all the instance variables that a person has as well as the two extra ones specified. It also means that a professor is receptive to all the methods that a person or an adult is, as well as the extra method -write_paper- which was (rather poorly) defined above, for at least two cases. vars marvin; consprofessor("marvin", 53, "male", "nina", '(691) 455 554', "computers_and_philosophy") -> marvin; marvin => ** discipline(marvin) => ** computers_and_philosophy birthday(marvin); ** [Happy birthday marvin - now aged 54] person_age(marvin) => ** 54 write_paper(marvin,conssubject("ai")); ** [marvin is writing a paper on ai] write_paper(marvin, roger); ** [marvin is writing a paper criticising penrose] -- Multiple Inheritance ------------------------------------------------ In many "object oriented" systems, though not all, you can specify that an objectclass should inherit from more than one objectclass. This feature is called multiple inheritance. Suppose that you wanted to create a special class of french professors. You could create an objectclass called french_professor which inherits from professor and defines all the features (instance variables and methods) that are special to french professors. define :class french_professor; is professor; /* features special to french professors */ enddefine; Alternatively you could define a french person objectclass which encompasses the features of frenchness and then mix the professor objectclass with the french person objectclass to create an objectclass which captures the features of frenchness and the the features of professordom. Given a french person objectclass you can create the french professor objectclass by doing: define :class french_professor; is professor, french_person; /* features special to french professors that are not true of professors in general or french people in general */ enddefine; The advantages of creating a french professor by this second method is that you can keep all features of french people together and quite separate from details which are only true of french professors. More importantly you now have an objectclass for a french person that you can mix with other objectclasses. For example if you define a student objectclass you can easily create a french student objectclass. If you do this then you can change some detail about french people and both the french professor objectclass and the french student objectclass will change automatically. In this sense the french person objectclass is a useful placeholder. Multiple Inheritance needs to be used with caution. For example, if you wanted to create a good toy truck objectclass, you could not simply start with the truck objectclass and toy objectclass and combine their features. Similarly, it is not possible to have a "good thing" objectclass and combine it with teacher and truck objectclasses to obtain good teacher and good truck objectclasses. REF *OBJECTCLASS/'Mixins' gives additional information on inheritance techniques. -- The Ordering of Components ------------------------------------------ Multiple inheritance can lead to a complex network, or lattice, or objectclass components from which an objectclass might inherit instance variables and methods. For example the objectclasses described above might be part of a larger collection with something like the following structure. /-------\ | ANIMAL| \+-+-+-+/ +---------+ | | | /--+---\ +-----+ | | |PERSON| | | +----+ \-+--+-/ | | | | | | | /+---------\ | +---|-----+ | |CHIMPANZEE| | | | | \----------/ /-+------+\ /+-+----------\ |PROFESSOR| |FRENCH PERSON| \------+--/ \-+-----------/ | | | | /--+--------+----\ |FRENCH PROFESSOR| \----------------/ The rules for inheritance in such cases are described in the TECAH *INHERITANCE. -- Daemons ------------------------------------------------------------- Sometimes an objectclass will want not only to use the method defined in one of its components but also to do some other action. For example if you have an object representing part of a diagram on a screen it may have parts of the diagram attached to it for which it is responsible. If the object receives a message to move then it will want to pass the message on to the parts of the diagram for which it is responsible. Another example might be that you want professors to check if they have reached retirement age whenever they get a "birthday" message. In both of these cases you could do this by redefining the original method for the subclass in question, but that would be wasteful. Instead you want to be able to say what happens to a subclass after (or before) a particular method inherited from a superclass is run, while leaving the inherited method unchanged, and preserving modularity of design. There are two ways that object oriented programming systems achieve this functionality. The first, adopted here is to have some syntax for saying within a method definition "now go and do what you would have done had this method not been here". The objectclass package uses the call_next_method syntax, illustrated below. (Some systems use a name something like "sendsuper" rather than "call_next_method") The alternative approach, followed in the Pop-11 FLAVOURS package, as explained in TEACH * FLAVOURS, is to use "before" and "after" methods, sometimes called "daemons". -- Example using call_next_method ------------------------------------- define :method birthday(prof:professor); lvars prof, age, name = person_name(prof); call_next_method(prof); person_age(prof) -> age; if age >= 65 then [^name is now retired] => elseif 65 - age < 5 then [^name has only ^(65 - age) years before retiring] => endif; enddefine; define :instance albert:professor; person_age = 63; person_name = "einstein"; discipline = "relativity"; enddefine; albert => ** birthday(albert); ** [Happy birthday einstein - now aged 64] ** [einstein has only 1 years before retiring] birthday(albert); ** [Happy birthday einstein - now aged 65] ** [einstein is now retired] birthday(albert); ** [Happy birthday einstein - now aged 66] ** [einstein is now retired] REF * CALL_NEXT_METHOD explains in more detail how this works. Just to check that instances that are not professors don't get this treatment: vars margaret = instance person; person_name = "margaret"; person_age = 63 endinstance; birthday(margaret); ** [Happy birthday margaret - now aged 64] birthday(margaret); ** [Happy birthday margaret - now aged 65] -- Updater Methods ---------------------------------------------------- The objectclasses package, like POP-11 itself, recognises a distinction between methods per se and methods invoked in the updater position. See HELP * DEFINE_METHOD For example the following are distinguished in that the first one runs the person_age method, and the second runs the person_age updater method. person_age(adam) -> x; x -> person_age(adam); -- Wrappers ----------------------------------------------------------- Wrappers are procedures that you associate with classes that can intercept construction, slot access, and slot update. You introduce wrappers with the following syntax vars gender; ;;; A simple construction-wrapper define making_new_animal() -> item; lvars item = apply(); ;;; construct the animal nprintf( 'Hooray, it\'s a %p', [% gender( item ) == "male" and "boy" or "girl" %] ) enddefine; ;;; Animals use this wrapper. define :class animal; slot gender = "female"; on cons do making_new_animal; enddefine; newanimal() => Hooray, it's a girl ** Instead of the word "cons" in the above example, you could use "access" to define a wrapper which is called when a slot is accessed or updated. Wrappers are inherit and are cumulative. At the moment, there is no way of avoiding the inheritance of your parent's wrappers. This is a restriction that I plan to remove at a later date, together with the restriction that a class inherits all slots, too. -- Computing slot defaults at compile time ---------------------------- When you specify a default for a slot, using define :class ; slot = ; ... enddefine; The expression is computed whenever an instance is created. This is very general. However, sometimes you would like to specify a default that is calculated at compile-time. This is sufficiently common that there is a special syntax for it: simply use "==" in place of the "=" sign. By writing == the is computed at compile time, i.e. when the definition is read in. The resulting value is then used as the default value for the slot when an instance is created. See TEACH * SLOT_DEFAULTS for a more extensive explanation. -- Methods can have untyped arguments --------------------------------- It is possible to define methods which mix typed and untyped arguments. For example someone who wishes to fake a different age might use this method, where the untyped argument "years" represents a pop-11 integer. define :method get_younger(years, p:person); lvars years, p, age = person_age(p); age - years -> age; age -> person_age(p); [^(person_name(p)) now has age ^age] => enddefine; birthday(albert); ** [Happy birthday einstein - now aged 67] ** [einstein is now retired] get_younger(10, albert); ** [einstein now has age 57] birthday(albert); ** [Happy birthday einstein - now aged 58] For more on this see HELP * DEFINE_METHOD -- Metaclasses -------------------------------------------------------- Some OOP systems have special classes called metaclasses whose instances are themselves classes. In LIB FLAVOURS these are called metaflavours. In the objectclass package metaclasses are not provided. For more on this see HELP * LIKE_FLAVOURS. The call_next_method and other facilities provide an alternative approach to some of the features of metaclasses. -- Bugs and Omissions -------------------------------------------------- -- See Also ------------------------------------------------------------ TEACH * OOPS For a general discussion of object oriented programming. HELP * RECORDCLASS REF * DEFSTRUCT These are worth reading since they are the facilities on which objectclass is closely based. TEACH * INHERITANCE TEACH * OBJ_INDEX List of teach files available HELP * OBJECTCLASS A more detailed overview HELP * OBJ_INDEX List of help files available REF * OBJECTCLASS REF * OBJ_INDEX List of REF files available The following gives information about other OOP facilities in Poplog. HELP * NEWOBJ TEACH * FLAVOURS This file also includes a useful overview of object oriented programming, and ends with a section on OOP jargon and a short bibliography HELP * FLAVOURS REF * FLAVOURS The Common Lisp Object System (CLOS, pronounced "kloss" by some people and "seeloss" by others!) is described in the second edition of G.L. Steele, Common Lisp: The Language Digital Press Burlington, MA 01803 --- C.all/lib/proto/objectclass/teach/objectclass_example --- Copyright University of Sussex 1993. All rights reserved.