@database beginner.guide @Master beginner @Width 75 This is the AmigaGuideŽ file beginner.guide, produced by Makeinfo-1.55 from the input file beginner. @NODE "main" "Object Oriented E" @Next "Examples.guide/main" @Prev "Recursion.guide/main" @Toc "Contents.guide/main" Object Oriented E ***************** The Object Oriented Programming (OOP) aspects of E are covered in this chapter. Don't worry if you don't know the OOP buzz words like `object', `method' and `inheritance': these terms are explained in the OOP introduction, below. (For some reason, computer science uses strange words to cloak simple concepts in secrecy.) @{" OOP Introduction " Link "OOP Introduction" } @{" Objects in E " Link "Objects in E" } @{" Methods in E " Link "Methods in E" } @{" Inheritance in E " Link "Inheritance in E" } @{" Data-Hiding in E " Link "Data-Hiding in E" } @ENDNODE @NODE "OOP Introduction" "OOP Introduction" @Next "Objects in E" @Toc "main" OOP Introduction ================ `Object Oriented Programming' is the name given to a collection of programming techniques that are meant to speed up development and ease maintenance of large programs. These techniques have been around for a long time, but it is only recently that languages that explicitly support them have become popular. You do not @{i }need@{ui } to use a language that supports OOP to program in an Object Oriented way; it's just a bit simpler if you do! @{" Classes and methods " Link "Classes and methods" } @{" Example class " Link "Example class" } @{" Inheritance " Link "Inheritance" } @ENDNODE @NODE "Classes and methods" "Classes and methods" @Next "Example class" @Toc "OOP Introduction" Classes and methods ------------------- The heart of OOP is the `Black Box' approach to programming. The kind of black box in question is one where the contents are unknown but there is a number of wires on the outside which give you some way of interacting with the stuff on the inside. The black boxes of OOP are actually collections of data (just like the idea of variables that we've already met) and they are called @{fg shine }objects@{fg text } (this is the general term, which is, coincidentally, connected with the @{b }OBJECT@{ub } type in E). Objects can be grouped together in @{fg shine }classes@{fg text }, like the types for variables, except that a class also defines what different kinds of wires protrude from the black box. This extra bit (the wires) is known as the @{fg shine }interface@{fg text } to the object, and is made up of a number of @{fg shine }methods@{fg text } (so a method is analogous to a wire). Each method is actually just like a procedure. With a real black box, the wires are the only way of interacting with the box, so the methods of an object ought to be the @{i }only@{ui } way of creating and using the object. Of course, the methods themselves normally need to know the internal workings of the object, just like the way the wires are normally connected to something inside the black box. There are two special kinds of methods: @{fg shine }constructors@{fg text } and @{fg shine }destructors@{fg text }. A @{fg shine }constructor@{fg text } is a method which is used to initialise the data in an object, and a class may have several different constructors (allowing for different kinds of initialisation) or it may have none if no special initialisation is necessary. Constructors are normally used to allocate the resources (such as memory) that an object needs. The deallocation of such resources is done by the @{fg shine }destructor@{fg text }, of which there is at most one for each class. Protecting the contents of an object in the `black box' way is known as @{fg shine }data-hiding@{fg text } (the data in the object is visible only to its methods), and only allowing the contents of an object to be manipulated via its interface is known as @{fg shine }data abstraction@{fg text }. By using this approach, only the methods know the structure of the data in an object and so this structure can be changed without affecting the whole of a program: only the methods would potentially need recoding. As you might be able to tell, this simplifies maintenance quite considerably. @ENDNODE @NODE "Example class" "Example class" @Next "Inheritance" @Prev "Classes and methods" @Toc "OOP Introduction" Example class ------------- A good example of a class is the mathematical notion of a set (of integers). A particular object from this class would represent a particular set of integers. The interface for the class would probably include the following methods: 1. @{b }Add@{ub } -- adds an integer to a set object. 2. @{b }Member@{ub } -- tests for membership of an integer in a set object. 3. @{b }Empty@{ub } -- tests for emptiness of a set object. 4. @{b }Union@{ub } -- unions a set object with a set object. A more complete class would also contain methods for removing elements, intersecting sets etc. The important thing to notice is that to use this class you need to know only how to use the methods. The black box approach means that we don't (and shouldn't) know how the set class is actually implemented, i.e., how data is structured within a set object. Only the methods themselves need to know how to manipulate the data that represents a set object. The benefit of OOP comes when you actually use the classes, so suppose you implement this set class and then use it in your code for some database program. If you found that the set implementation was a bit inefficient (in terms of memory or speed), then, since you programmed in this OOP way, you wouldn't have to recode the whole database program, just the set class! You can change the way the set data is structured in an object as much and as often as you like, so long as each implementation has the same interface (and gives the same results!). @ENDNODE @NODE "Inheritance" "Inheritance" @Prev "Example class" @Toc "OOP Introduction" Inheritance ----------- The remaining OOP concept of interest is @{fg shine }inheritance@{fg text }. This is a grand name for a way of building on classes that enables the @{fg shine }derived@{fg text } (i.e., bigger) class to be used as if its objects were really members of the inherited, or @{fg shine }base@{fg text }, class. For example, suppose class @{b }D@{ub } were derived from class @{b }B@{ub }, so @{b }D@{ub } is the derived class and @{b }B@{ub } is the base class. In this case, class @{b }D@{ub } inherits the data structure of class @{b }B@{ub }, and may add extra data to it. It also inherits all the methods of class @{b }B@{ub }, and objects of class @{b }D@{ub } may be treated as if they were really objects of class @{b }B@{ub }. Of course, an inherited method cannot affect the extra data in class @{b }D@{ub }, only the inherited data. To affect the extra data, class @{b }D@{ub } can have extra methods defined, or it can make new definitions for the inherited methods. The latter approach is only really useful if the new definition of an inherited method is pretty similar to the inherited method, differing only in how it affects the extra data in class @{b }D@{ub }. This overriding of methods does not affect the methods in class @{b }B@{ub } (nor those of other classes derived from @{b }B@{ub }), but only those in class @{b }D@{ub } and the classes derived from @{b }D@{ub }. @ENDNODE @NODE "Objects in E" "Objects in E" @Next "Methods in E" @Prev "OOP Introduction" @Toc "main" Objects in E ============ Classes are defined using @{b }OBJECT@{ub } in the same way that we've seen before (see @{"OBJECT Type" Link "Types.guide/OBJECT Type" }). So, in E, the terms `object declaration' and `class' may be used interchangeably. However, referring to an @{b }OBJECT@{ub } type as a `class' signals the presence of methods in an object. The following example @{b }OBJECT@{ub } is the basis of a set class, as described above (see @{"Example class" Link "Example class" }). This set implementation is going to be quite simple and it will be limited to a maximum of 100 elements. OBJECT set elts[100]:ARRAY OF LONG size ENDOBJECT Currently, the only way to allocate an OOP object is to use @{b }NEW@{ub } with an appropriately typed pointer. The following sections of code all allocate memory for the data of @{b }set@{ub }, but only the last one allocates an OOP @{b }set@{ub } object. Each one may use and access the @{b }set@{ub } data, but only the last one may call the methods of @{b }set@{ub }. DEF s:set DEF s:PTR TO set s:=NewR(SIZEOF set) DEF s:PTR TO set s:=NEW s OOP objects can, of course, be deallocated using @{b }END@{ub }, in which case the destructor for the corresponding class is also called. Leaving an OOP object to be deallocated automatically at the end of the program is not quite as safe as normal, since in this case the destructor will not be called. Also, when using @{b }END@{ub } to deallocate an object you do not need to use a pointer of exactly the same type as the object (like you would for normal @{b }NEW@{ub } allocations). Instead you can use a pointer of any of the base classes' types. Constructors and destructors are described in more detail below. @ENDNODE @NODE "Methods in E" "Methods in E" @Next "Inheritance in E" @Prev "Objects in E" @Toc "main" Methods in E ============ The @{fg shine }methods@{fg text } of E are very similar to normal procedures, but there is one, big difference: a method is part of a class, so must somehow be identified with the other parts of the class. In E this identification is done by relating all methods to the corresponding @{b }OBJECT@{ub } type for the class, using the @{b }OF@{ub } keyword after the description of the method's parameters. So, the methods of the simple set class would be defined as outlined below (of course, these examples have omitted the code of methods). PROC add(x) OF set /* code for add method */ ENDPROC PROC member(x) OF set /* code for member method */ ENDPROC PROC empty() OF set /* code for empty method */ ENDPROC PROC union(s:PTR TO set) OF set /* code for union method */ ENDPROC At first sight it might seem that the particular @{b }set@{ub } object which would be manipulated by these methods is missing from the parameters. For instance, it appears that the @{b }empty@{ub } method should need an extra @{b }PTR TO set@{ub } parameter, and that would be the @{b }set@{ub } object it tested for emptiness. However, methods are called in a slightly different way to normal procedures. A method is a part of a class, and is called in a similar way to accessing the data elements of the class. That is, the method is selected using @{b }.@{ub } and acts (implicitly) on the object from which it was selected. The following example shows the allocation of a set object and the use of some of the above methods. DEF s:PTR TO set NEW s -> Allocate an OOP object s.add(17) s.add(-34) IF s.empty() WriteF('Error: the set s should not be empty!\\n') ELSE WriteF('OK: not empty\\n') ENDIF IF s.member(0) WriteF('Error: how did 0 get in there?\\n') ELSE WriteF('OK: 0 is not a member\\n') ENDIF IF s.member(-34) WriteF('OK: -34 is a member\\n') ELSE WriteF('Error: where has -34 gone?\\n') ENDIF END s -> Finished with s now This is why the methods do not take that extra @{b }PTR TO set@{ub } argument. If a method is called then it has been selected from an appropriate object, and so this must be the object which it affects. The slightly complicated method is @{b }union@{ub } which adds another @{b }set@{ub } object by unioning it. In this case, the argument to the method is a @{b }PTR TO set@{ub }, but this is the set to be added, not the set which is being expanded. So, how do you refer to the object which is being affected? In other words, how do you affect it? Well, this is the remaining difference from normal procedures: every method has a special local variable, @{b }self@{ub }, which is of type @{b }PTR TO @{ub }@{fg shine }class@{fg text }@{b }@{ub } and is initialised to point to the object from which the method was selected. Using this variable, the data and methods of object can be accessed and used as normal. For instance, the @{b }empty@{ub } method has a @{b }self@{ub } local variable of type @{b }PTR TO set@{ub }, and can be defined as below: PROC empty() OF set IS self.size=0 @{fg shine }Constructors@{fg text } are simply methods which initialise the data of an object. For this reason they should normally be called only when the object is allocated. The @{b }NEW@{ub } operator allows OOP objects to call a constructor at the point at which they are allocated, to make this easier and more explicit. The constructor will be called after @{b }NEW@{ub } has allocated the memory for the object. It is wise to give constructors suggestive names like @{b }create@{ub } and @{b }copy@{ub }, or the same name as the class. The following constructors might be defined for the set class: /* Create empty set */ PROC create() OF set self.size=0 ENDPROC /* Copy existing set */ PROC copy(oldset:PTR TO set) OF set DEF i FOR i:=0 TO oldset.size-1 self.elements[i]:=oldset.elements[i] ENDFOR self.size:=oldset.size ENDPROC They would be used as in the code below. Notice that the @{b }create@{ub } constructor is, in this case, redundant since @{b }NEW@{ub } will initialise the data elements to zero. If @{b }NEW@{ub } does sufficient initialisation then you do not have to define any constructors, and even if you do have constructors you don't @{i }have@{ui } to use them when allocating objects. DEF s:PTR TO set, t:PTR TO set, u:PTR TO set NEW s.create() IF s.empty THEN WriteF('s is empty\\n') END s NEW t /* This happens to be the same as using create */ IF t.empty THEN WriteF('t is empty\\n') t.add(10) NEW u.copy(t) IF u.member(10) THEN WriteF('10 is in u\\n') END t, u For each class there is at most one @{fg shine }destructor@{fg text }, and this is responsible for clearing up and deallocating resources. If one is needed then it must be called @{b }end@{ub }, and (as this might suggest) it is called automatically when an OOP object is deallocated using @{b }END@{ub }. So, for OOP objects with a destructor, the (roughly) equivalent code to @{b }END@{ub } using @{b }Dispose@{ub } is a bit different. Take care to note that the destructor is @{i }not@{ui } called if @{b }END@{ub } is not used to deallocate an OOP object (i.e., if deallocation is left to be done automatically at the end of the program). END p IF p p.end() -> Call destructor Dispose(p) p:=NIL ENDIF The simple implementation of the set class needs no destructor. If, however, the @{b }elements@{ub } data were a pointer (to @{b }LONG@{ub }), and the array were allocated based on some size parameter to a constructor, then a destructor would be useful. In this case the set class would also need a @{b }maxsize@{ub } data element, which records the maximum, allocated size of the @{b }elements@{ub } array. OBJECT set elements:PTR TO LONG size maxsize ENDOBJECT PROC create(sz=100) OF set -> Default to 100 DEF p:PTR TO LONG self.maxsize:=IF (sz>0) AND (sz<100000) THEN sz ELSE 100 self.elements:=NEW p[self.maxsize] ENDPROC PROC end() OF set DEF p:PTR TO LONG IF self.maxsize=0 WriteF('Error: did not create() the set\\n') ELSE p:=self.elements END p[self.maxsize] ENDIF ENDPROC Without the destructor @{b }end@{ub }, the memory allocated for @{b }elements@{ub } would not be deallocated when @{b }END@{ub } is used, although it would get deallocated at the end of the program (in this case). However, if @{b }AllocMem@{ub } were used instead of @{b }NEW@{ub } to allocate the array, then the memory would have to be deallocated using @{b }FreeMem@{ub }, and this would best be done in the destructor, as above. (The memory would not be deallocated automatically at the end of the program if @{b }AllocMem@{ub } is used.) Another solution to this kind of problem would be to have a special method which called @{b }FreeMem@{ub }, and to remember to call this method just before deallocating one of these objects, so you can see that the interaction of @{b }END@{ub } with destructors is quite useful. Already, the above re-definition of @{b }set@{ub } begins to show the power of OOP. The actual implementation of the set class is very different, but the interface can remain the same. The code for the methods would need to change to take into account the new @{b }maxsize@{ub } element (where before the fixed size of 100 was used), and also to deal with the possibility the @{b }create@{ub } constructor had not been used (in which case @{b }elements@{ub } would be @{b }NIL@{ub } and @{b }maxsize@{ub } zero). But the code which used the set class would not need to change, except maybe to allocate more sensibly sized sets! Yet another, different implementation of a set was outlined above (see @{"Binary Trees" Link "Recursion.guide/Binary Trees" }). In fact, remarkably few changes would be needed to convert the code from that section into another implementation of the set class. The @{b }new_set@{ub } procedure is like a set constructor which initialises the set to be a singleton (i.e., to contain one element), and the @{b }add@{ub } procedure is just like the @{b }add@{ub } method of the set class. The only slight problem is that empty sets are not modelled by the binary tree implementation, so it wouldn't, as it stands, be a complete implementation. It would be straight-forward (but unduly complicated at this point) to add support for empty sets to this particular implementation. @ENDNODE @NODE "Inheritance in E" "Inheritance in E" @Next "Data-Hiding in E" @Prev "Methods in E" @Toc "main" Inheritance in E ================ One class is @{fg shine }derived@{fg text } from another using the @{b }OF@{ub } keyword in the definition of the derived class @{b }OBJECT@{ub }, in a similar way that @{b }OF@{ub } is used with methods. For instance, the following code shows how to define the class @{b }d@{ub } to be derived from class @{b }b@{ub }. The class @{b }b@{ub } is then said to be @{fg shine }inherited@{fg text } by the class @{b }d@{ub }. OBJECT b b_data ENDOBJECT OBJECT d OF b extra_d_data ENDOBJECT The names @{b }b@{ub } and @{b }d@{ub } have been chosen to be somewhat suggestive, since the class which is inherited (i.e., @{b }b@{ub }) is known as the @{fg shine }base@{fg text } class, whilst the inheriting class (i.e., @{b }d@{ub }) is known as the @{fg shine }derived@{fg text } class. The definition of @{b }d@{ub } is the same as the following definition of @{b }duff@{ub }, except for one major difference: with the above derivation the methods of @{b }b@{ub } are also inherited by @{b }d@{ub } and they become methods of class @{b }d@{ub }. The definition of @{b }duff@{ub } relates it in no way to @{b }b@{ub }, except at best accidentally (since any changes to @{b }b@{ub } do not affect @{b }duff@{ub }, whereas they would affect @{b }d@{ub }). OBJECT duff b_data extra_d_data ENDOBJECT One property of this derivation applies to the data records built by @{b }OBJECT@{ub } as well as the OOP classes. The data records of type @{b }d@{ub } or @{b }duff@{ub } may be used wherever a data record of type @{b }b@{ub } were required (e.g., the argument to some procedure), and they are, in fact, indistinguishable from records of type @{b }b@{ub }. Although, if the definition of @{b }b@{ub } were changed (e.g., by changing the name of the @{b }b_data@{ub } element) then data records of type @{b }duff@{ub } would not be usable in this way, but those of type @{b }d@{ub } still would. Therefore, it is wise to use inheritance to show the relationships between classes or data of @{b }OBJECT@{ub } types. The following example shows how procedure @{b }print_b_data@{ub } can validly be called in several ways, given the definitions of @{b }b@{ub }, @{b }d@{ub } and @{b }duff@{ub } above. PROC print_b_data(p:PTR TO b) WriteF('b_data = \\d\\n', p.b_data) ENDPROC PROC main() DEF p_b:PTR TO b, p_d:PTR TO d, p_duff:PTR TO duff NEW p_b, p_d, p_duff p_b.b_data:=11 p_d.b_data:=-3 p_duff.b_data:=27 WriteF('Printing p_b: ') print_b_data(p_b) WriteF('Printing p_d: ') print_b_data(p_d) WriteF('Printing p_duff: ') print_b_data(p_duff) ENDPROC So far, no methods have been defined for @{b }b@{ub }, which means that it is just an @{b }OBJECT@{ub } type. The procedure @{b }print_b_data@{ub } suggests a useful method of @{b }b@{ub }, which will be called @{b }print@{ub }. PROC print() OF b WriteF('b_data = \\d\\n', self.b_data) ENDPROC This definition would also define a @{b }print@{ub } method for @{b }d@{ub }, since @{b }d@{ub } is derived from @{b }b@{ub } and it inherits all the methods of @{b }b@{ub }. However, @{b }duff@{ub } would, of course, still be just an @{b }OBJECT@{ub } type, although it could have a similar @{b }print@{ub } method explicitly defined for it. If @{b }b@{ub } has any methods defined for it (i.e., if it is a class) then data records of type @{b }duff@{ub } cannot be used as if they were @{i }objects@{ui } of the class @{b }b@{ub }, and it is not safe to try! In this case, only objects of derived class @{b }d@{ub } can be used in this manner. (If @{b }b@{ub } is a class then @{b }d@{ub } is a class, due to inheritance.) PROC main() DEF p_b:PTR TO b, p_d:PTR TO d, p_duff:PTR TO duff NEW p_b, p_d, p_duff p_b.b_data:=11 p_d.b_data:=-3; p_d.extra_d_data:=3 p_duff.b_data:=7; p_duff.extra_d_data:=-7 WriteF('Printing p_b: ') /* b explicitly has print method */ p_b.print() WriteF('Printing p_d: ') /* d inherits print method from b */ p_d.print() WriteF('No print method for p_duff\\n') /* Do not try to print p_duff in this way */ /* p_duff.print() */ ENDPROC Unfortunately, the @{b }print@{ub } method inherited by @{b }d@{ub } only prints the @{b }b_data@{ub } element (since it is really a method of @{b }b@{ub }, so cannot access the extra data added in @{b }d@{ub }). However, any inherited method can be overridden by defining it again, this time for the derived class. PROC print() OF d WriteF('extra_d_data = \\d, ', self.extra_d_data) WriteF('b_data = \\d\\n', self.b_data) ENDPROC With this extra definition, the same @{b }main@{ub } procedure above would now print all the data of @{b }d@{ub }, but only the @{b }b_data@{ub } element of @{b }b@{ub }. This is because the new definition of @{b }print@{ub } affects only class @{b }d@{ub } (and classes derived from @{b }d@{ub }). Inherited methods are often overridden just to add extra functionality, as in the case above where we wanted the extra data to be printed as well as the data derived from @{b }b@{ub }. For this purpose, the @{b }SUPER@{ub } operator can be used on a method call to force the base class method to be used, where normally the derived class method would be used. So, the definition of the @{b }print@{ub } method for class @{b }d@{ub } could call the @{b }print@{ub } method of class @{b }b@{ub }. PROC print() OF d WriteF('extra_d_data = \\d, ', self.extra_d_data) SUPER self.print() ENDPROC Be careful, though, because without the @{b }SUPER@{ub } operator this would involve a recursive call to the @{b }print@{ub } method of class @{b }d@{ub }, rather than a call to the base class method. Just as data records of type @{b }d@{ub } can be used wherever data records of type @{b }b@{ub } were required, objects of class @{b }d@{ub } can used in place of objects of class @{b }b@{ub }. The following procedure prints a message and the object data, using the @{b }print@{ub } method of @{b }b@{ub }. (Of course, only the methods named by class @{b }b@{ub } can be used in such a procedure, since the pointer @{b }p@{ub } is of type @{b }PTR TO b@{ub }.) PROC msg_print(msg, p:PTR TO b) WriteF('Printing \\s: ', msg) p.print() ENDPROC PROC main() DEF p_b:PTR TO b, p_d:PTR TO d NEW p_b, p_d p_b.b_data:=11 p_d.b_data:=-3; p_d.extra_d_data:=3 msg_print('p_b', p_b) msg_print('p_d', p_d) ENDPROC You can't use @{b }duff@{ub } now, since it is not a class and @{b }b@{ub } is, and @{b }msg_print@{ub } expects a pointer to class @{b }b@{ub }. The only other objects that can be passed to @{b }msg_print@{ub } are objects from classes derived from @{b }b@{ub }, and this is why @{b }p_d@{ub } can be printed using @{b }msg_print@{ub }. If you collect together the code and run the example you will see that the call to @{b }print@{ub } in @{b }msg_print@{ub } uses the overridden @{b }print@{ub } method when @{b }msg_print@{ub } is called with @{b }p_d@{ub } as a parameter. That is, the correct method is called even though the pointer @{b }p@{ub } is not of type @{b }PTR TO d@{ub }. This is called @{fg shine }polymorphism@{fg text }: different implementations of @{b }print@{ub } may be called depending on the real, @{fg shine }dynamic@{fg text } type of @{b }p@{ub }. Here's what should be printed: Printing p_b: b_data = 11 Printing p_d: extra_d_data = 3, b_data = -3 Inheritance is not limited to a single layer: you can derive other classes from @{b }b@{ub }, you can derive classes from @{b }d@{ub }, and so on. For instance, if class @{b }e@{ub } is derived from class @{b }d@{ub } then it would inherit all the data of @{b }d@{ub } and all the methods of @{b }d@{ub }. This means that @{b }e@{ub } would inherit the richer version of @{b }print@{ub }, and may even override it yet again. In this case, class @{b }e@{ub } would have two base classes, @{b }b@{ub } and @{b }d@{ub }, but would be derived directly from @{b }d@{ub } (and indirectly from @{b }b@{ub }, via @{b }d@{ub }). Class @{b }d@{ub } would therefore be known as the @{fg shine }super@{fg text } class of @{b }e@{ub }, since @{b }e@{ub } is derived directly from @{b }d@{ub }. (The super class of @{b }d@{ub } is its only base class, @{b }b@{ub }.) So, the @{b }SUPER@{ub } operator is actually used to call the methods in the super class. In this example, the @{b }SUPER@{ub } operator can be used in the methods of @{b }e@{ub } to call methods of @{b }d@{ub }. The binary tree implementation above (see @{"Binary Trees" Link "Recursion.guide/Binary Trees" }) suggests a good example for a @{fg shine }class hierarchy@{fg text } (a collection of classes related by inheritance). A basic tree structure can be encapsulated in a base class definition, and then specific kinds of tree (with different data at the nodes) can be derived from this. In fact, the base class @{b }tree@{ub } defined below is only useful for inheriting, since a tree is pretty useless without some data attached to the nodes. Since it is very likely that objects of class @{b }tree@{ub } will never be useful (but objects of classes derived from @{b }tree@{ub } would be), the @{b }tree@{ub } class is called an @{fg shine }abstract@{fg text } class. OBJECT tree left:PTR TO tree, right:PTR TO tree ENDOBJECT PROC nodes() OF tree DEF tot=1 IF self.left THEN tot:=tot+self.left.nodes() IF self.right THEN tot:=tot+self.right.nodes() ENDPROC tot PROC leaves(show=FALSE) OF tree DEF tot=0 IF self.left tot:=tot+self.left.leaves(show) ENDIF IF self.right tot:=tot+self.right.leaves(show) ELSEIF self.left=NIL IF show THEN self.print_node() tot++ ENDIF ENDPROC tot PROC print_node() OF tree WriteF(' ') ENDPROC PROC print() OF tree IF self.left THEN self.left.print() self.print_node() IF self.right THEN self.right.print() ENDPROC The @{b }nodes@{ub } and @{b }leaves@{ub } methods return the number of nodes and leaves of the tree, respectively, with the @{b }leaves@{ub } method taking a flag to specify whether the leaves should also be printed. These methods should never need overriding in a class derived from @{b }tree@{ub }, and neither should @{b }print@{ub }, which traverses the tree, printing the nodes from left to right. However, the @{b }print_node@{ub } method probably should be overridden, as is the case in the integer tree defined below. OBJECT integer_tree OF tree int ENDOBJECT PROC create(i) OF integer_tree self.int:=i ENDPROC PROC add(i) OF integer_tree DEF p:PTR TO integer_tree IF i < self.int IF self.left p:=self.left p.add(i) ELSE self.left:=NEW p.create(i) ENDIF ELSEIF i > self.int IF self.right p:=self.right p.add(i) ELSE self.right:=NEW p.create(i) ENDIF ENDIF ENDPROC PROC print_node() OF integer_tree WriteF('\\d ', self.int) ENDPROC This is a nice example of polymorphism at work: we can implement a @{b }tree@{ub } which works with integers simply by defining the appropriate methods. The @{b }leaves@{ub } method (of the @{b }tree@{ub } class) will then automatically call the @{b }integer_tree@{ub } version of @{b }print_node@{ub } whenever we pass it an @{b }integer_tree@{ub } object. The definitions of @{b }tree@{ub } and @{b }integer_tree@{ub } can even be in different modules (see @{"Data-Hiding in E" Link "Data-Hiding in E" }), and, using these OOP techniques, the module containing @{b }tree@{ub } would not need to be recompiled even if a class like @{b }integer_tree@{ub } is added or changed. This shows why OOP is good for code-reuse and extensibility: with traditional programming techniques we would have to adapt the binary tree functions to account for integers, and again for each new datatype. Notice that the recursive use of the new method @{b }add@{ub } must be called via an auxiliary pointer, @{b }p@{ub }, of the derived class. This is because the @{b }left@{ub } and @{b }right@{ub } elements of @{b }tree@{ub } are pointers to @{b }tree@{ub } objects and @{b }add@{ub } is not a method of @{b }tree@{ub } (the compiler would reject the code as a syntax error if you tried to directly access @{b }add@{ub } under these circumstances). Of course, if the @{b }tree@{ub } class had an @{b }add@{ub } method there would not be this problem, but what would the code be for such a method? An @{b }add@{ub } method does not really make sense for @{b }tree@{ub }, but if almost all classes derived from @{b }tree@{ub } are going to need such a method it might be nice to include it in the @{b }tree@{ub } base class. This is the purpose of @{fg shine }abstract@{fg text } methods. An @{fg shine }abstract@{fg text } method is one which exists in a base class solely so that it can be overridden in some derived class. Normally, such methods have no sensible definition in the base class, so there is a special keyword, @{b }EMPTY@{ub }, which can be used to define them. For example, the @{b }add@{ub } method in @{b }tree@{ub } would be defined as below. PROC add(x) OF tree IS EMPTY With this definition, the code for the @{b }add@{ub } method for the @{b }integer_tree@{ub } class could be simplified. (The auxiliary pointer, @{b }p@{ub }, is still needed for use with @{b }NEW@{ub }, since an expression like @{b }self.left@{ub } is not a pointer variable.) PROC add(i) OF integer_tree DEF p:PTR TO integer_tree IF i < self.int IF self.left self.left.add(i) ELSE self.left:=NEW p.create(i) ENDIF ELSEIF i > self.int IF self.right self.right.add(i) ELSE self.right:=NEW p.create(i) ENDIF ENDIF ENDPROC This, however, is not the best example of an abstract method, since the @{b }add@{ub } method in every class derived from @{b }tree@{ub } must now take a single @{b }LONG@{ub } value as an parameter, in order to be compatible. In general, though, a class representing a tree with node data of type @{fg shine }t@{fg text } would really want an @{b }add@{ub } method to take a single parameter of type @{fg shine }t@{fg text }. The fact that a @{b }LONG@{ub } value can represent a pointer to any type is helpful, here. This means that the definition of @{b }add@{ub } may not be so limiting, after all. The @{b }print_node@{ub } method is much more obviously suited to being an abstract method. The above definition prints something silly, because at that point we didn't know about abstract methods and we needed the method to be defined in the base class. A much better definition would make @{b }print_node@{ub } abstract. PROC print_node() OF tree IS EMPTY It is quite safe to call these abstract methods, even for @{b }tree@{ub } class objects. If a method is still abstract in any class (i.e., it has not been overridden), then calling it on objects of that class has the same effect as calling a function which just returns zero (i.e., it does very little!). The @{b }integer_tree@{ub } class could be used like this: PROC main() DEF t:PTR TO integer_tree NEW t.create(10) t.add(-10) t.add(3) t.add(5) t.add(-1) t.add(1) WriteF('t has \\d nodes, with \\d leaves: ', t.nodes(), t.leaves()) t.leaves(TRUE) WriteF('\\n') WriteF('Contents of t: ') t.print() WriteF('\\n') END t ENDPROC @ENDNODE @NODE "Data-Hiding in E" "Data-Hiding in E" @Prev "Inheritance in E" @Toc "main" Data-Hiding in E ================ @{fg shine }Data-hiding@{fg text } is accomplished in E at the module level. This means, effectively, that it is wise to define classes in separate modules (or at least only closely related classes together in a module), taking care to @{b }EXPORT@{ub } only the definitions that you need to. You can also use the @{b }PRIVATE@{ub } keyword in the definition of any @{b }OBJECT@{ub } to hide all the elements following it from code which uses the module (although this does not affect the code within the module). The @{b }PUBLIC@{ub } keyword can be used in a similar way to make the elements which follow visible (i.e., accessible) again, as they are by default. For instance, the following @{b }OBJECT@{ub } definition makes @{b }x@{ub }, @{b }y@{ub }, @{b }a@{ub } and @{b }b@{ub } private (so only visible to the code within the same module), and @{b }p@{ub }, @{b }q@{ub } and @{b }r@{ub } public (so visible to code external to the module, too). OBJECT rec p:INT PRIVATE x:INT y PUBLIC q r:PTR TO LONG PRIVATE a:PTR TO LONG, b ENDOBJECT For the set class you would probably want to make all the data private and all the methods public. In this way you force programs which use this module to use the supplied interface, rather than fiddling with the set data structures themselves. The following example is the complete code for a simple, inefficient set class, and can be compiled to a module. OPT MODULE -> Define class 'set' in a module OPT EXPORT -> Export everything /* The data for the class */ OBJECT set PRIVATE -> Make all the data private elements:PTR TO LONG maxsize, size ENDOBJECT /* Creation constructor */ /* Minimum size of 1, maximum 100000, default 100 */ PROC create(sz=100) OF set DEF p:PTR TO LONG self.maxsize:=IF (sz>0) AND (sz<100000) THEN sz ELSE 100 -> Check size self.elements:=NEW p[self.maxsize] ENDPROC /* Copy constructor */ PROC copy(oldset:PTR TO set) OF set DEF i self.create(oldset.maxsize) -> Call create method! FOR i:=0 TO oldset.size-1 -> Copy elements self.elements[i]:=oldset.elements[i] ENDFOR self.size:=oldset.size ENDPROC /* Destructor */ PROC end() OF set DEF p:PTR TO LONG IF self.maxsize<>0 -> Check that it was allocated p:=self.elements END p[self.maxsize] ENDIF ENDPROC /* Add an element */ PROC add(x) OF set IF self.member(x)=FALSE -> Is it new? (Call member method!) IF self.size=self.maxsize Raise("full") -> The set is already full ELSE self.elements[self.size]:=x self.size:=self.size+1 ENDIF ENDIF ENDPROC /* Test for membership */ PROC member(x) OF set DEF i FOR i:=0 TO self.size-1 IF self.elements[i]=x THEN RETURN TRUE ENDFOR ENDPROC FALSE /* Test for emptiness */ PROC empty() OF set IS self.size=0 /* Union (add) another set */ PROC union(other:PTR TO set) OF set DEF i FOR i:=0 TO other.size-1 self.add(other.elements[i]) -> Call add method! ENDFOR ENDPROC /* Print out the contents */ PROC print() OF set DEF i WriteF('{ ') FOR i:=0 TO self.size-1 WriteF('\\d ', self.elements[i]) ENDFOR WriteF('}') ENDPROC This class can be used in another module or program, as below: MODULE '*set' PROC main() HANDLE DEF s=NIL:PTR TO set NEW s.create(20) s.add(1) s.add(-13) s.add(91) s.add(42) s.add(-76) IF s.member(1) THEN WriteF('1 is a member\\n') IF s.member(11) THEN WriteF('11 is a member\\n') WriteF('s = ') s.print() WriteF('\\n') EXCEPT DO END s SELECT exception CASE "NEW" WriteF('Out of memory\\n') CASE "full" WriteF('Set is full\\n') ENDSELECT ENDPROC @ENDNODE