Go to the Next or Previous section, the Detailed Contents, or the Amiga E Encyclopedia.


17.4 Inheritance in E

One class is derived from another using the OF keyword in the definition of the derived class OBJECT, in a similar way that OF is used with methods. For instance, the following code shows how to define the class d to be derived from class b. The class b is then said to be inherited by the class d.

OBJECT b
  b_data
ENDOBJECT

OBJECT d OF b
  extra_d_data
ENDOBJECT

The names b and d have been chosen to be somewhat suggestive, since the class which is inherited (i.e., b) is known as the base class, whilst the inheriting class (i.e., d) is known as the derived class.

The definition of d is the same as the following definition of duff, except for one major difference: with the above derivation the methods of b are also inherited by d and they become methods of class d. The definition of duff relates it in no way to b, except at best accidentally (since any changes to b do not affect duff, whereas they would affect d).

OBJECT duff
  b_data
  extra_d_data
ENDOBJECT

One property of this derivation applies to the data records built by OBJECT as well as the OOP classes. The data records of type d or duff may be used wherever a data record of type b were required (e.g., the argument to some procedure), and they are, in fact, indistinguishable from records of type b. Although, if the definition of b were changed (e.g., by changing the name of the b_data element) then data records of type duff would not be usable in this way, but those of type d still would. Therefore, it is wise to use inheritance to show the relationships between classes or data of OBJECT types. The following example shows how procedure print_b_data can validly be called in several ways, given the definitions of b, d and duff 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, which means that it is just an OBJECT type. The procedure print_b_data suggests a useful method of b, which will be called print.

PROC print() OF b
  WriteF('b_data = \d\n', self.b_data)
ENDPROC

This definition would also define a print method for d, since d is derived from b and it inherits all the methods of b. However, duff would, of course, still be just an OBJECT type, although it could have a similar print method explicitly defined for it. If b has any methods defined for it (i.e., if it is a class) then data records of type duff cannot be used as if they were objects of the class b, and it is not safe to try! In this case, only objects of derived class d can be used in this manner. (If b is a class then d 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 print method inherited by d only prints the b_data element (since it is really a method of b, so cannot access the extra data added in d). 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 main procedure above would now print all the data of d, but only the b_data element of b. This is because the new definition of print affects only class d (and classes derived from d).

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. For this purpose, the SUPER 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 print method for class d could call the print method of class b.

PROC print() OF d
  WriteF('extra_d_data = \d, ', self.extra_d_data)
  SUPER self.print()
ENDPROC

Be careful, though, because without the SUPER operator this would involve a recursive call to the print method of class d, rather than a call to the base class method.

Just as data records of type d can be used wherever data records of type b were required, objects of class d can used in place of objects of class b. The following procedure prints a message and the object data, using the print method of b. (Of course, only the methods named by class b can be used in such a procedure, since the pointer p is of type PTR TO b.)

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 duff now, since it is not a class and b is, and msg_print expects a pointer to class b. The only other objects that can be passed to msg_print are objects from classes derived from b, and this is why p_d can be printed using msg_print. If you collect together the code and run the example you will see that the call to print in msg_print uses the overridden print method when msg_print is called with p_d as a parameter. That is, the correct method is called even though the pointer p is not of type PTR TO d. This is called polymorphism: different implementations of print may be called depending on the real, dynamic type of p. 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, you can derive classes from d, and so on. For instance, if class e is derived from class d then it would inherit all the data of d and all the methods of d. This means that e would inherit the richer version of print, and may even override it yet again. In this case, class e would have two base classes, b and d, but would be derived directly from d (and indirectly from b, via d). Class d would therefore be known as the super class of e, since e is derived directly from d. (The super class of d is its only base class, b.) So, the SUPER operator is actually used to call the methods in the super class. In this example, the SUPER operator can be used in the methods of e to call methods of d.

The binary tree implementation above (see 16.3 Binary Trees) suggests a good example for a class hierarchy (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 tree 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 tree will never be useful (but objects of classes derived from tree would be), the tree class is called an abstract 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('<NULL> ')
ENDPROC

PROC print() OF tree
  IF self.left  THEN self.left.print()
  self.print_node()
  IF self.right THEN self.right.print()
ENDPROC

The nodes and leaves methods return the number of nodes and leaves of the tree, respectively, with the leaves method taking a flag to specify whether the leaves should also be printed. These methods should never need overriding in a class derived from tree, and neither should print, which traverses the tree, printing the nodes from left to right. However, the print_node 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 tree which works with integers simply by defining the appropriate methods. The leaves method (of the tree class) will then automatically call the integer_tree version of print_node whenever we pass it an integer_tree object. The definitions of tree and integer_tree can even be in different modules (see 17.5 Data-Hiding in E), and, using these OOP techniques, the module containing tree would not need to be recompiled even if a class like integer_tree 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 add must be called via an auxiliary pointer, p, of the derived class. This is because the left and right elements of tree are pointers to tree objects and add is not a method of tree (the compiler would reject the code as a syntax error if you tried to directly access add under these circumstances). Of course, if the tree class had an add method there would not be this problem, but what would the code be for such a method?

An add method does not really make sense for tree, but if almost all classes derived from tree are going to need such a method it might be nice to include it in the tree base class. This is the purpose of abstract methods. An abstract 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, EMPTY, which can be used to define them. For example, the add method in tree would be defined as below.

PROC add(x) OF tree IS EMPTY

With this definition, the code for the add method for the integer_tree class could be simplified. (The auxiliary pointer, p, is still needed for use with NEW, since an expression like self.left 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 add method in every class derived from tree must now take a single LONG value as an parameter, in order to be compatible. In general, though, a class representing a tree with node data of type t would really want an add method to take a single parameter of type t. The fact that a LONG value can represent a pointer to any type is helpful, here. This means that the definition of add may not be so limiting, after all.

The print_node 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 print_node abstract.

PROC print_node() OF tree IS EMPTY

It is quite safe to call these abstract methods, even for tree 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 integer_tree 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


Go to the Next or Previous section, the Detailed Contents, or the Amiga E Encyclopedia.