COS 441 - Inheritance - April 23, 1996


Inheritance is a way of building new classes whose objects have similar behavior to existing classes, but add or replace certain methods. To add inheritance to our interpreter, we add a superclass expression to class, as well as a special class constructor root for base classes to inherit from. Now an object contains a list of object and method environments--earlier elements in the list are for subclasses. The new operation builds these lists in objects. Finally, send must now search for a method by first checking the first method environment in the object's list, then the second, and so on.
	(Class (super class-binds inst-binds methods)
	  (let ((s (eval super env))
		(class-env (extend-list env ...))
	        (meth-env (extend-list empty-env ...)))
	    (make-class s inst-binds meth-env class-env)))
        (Root () (make-Root))
        (New (class)
	  (letrec ((make (lambda (class)
			   (variant-case class
			     (Root () '())
			     (Class (super inst-binds meth-env class-env)
			       (let ((obj-env (extend-list class-env ...))
				     (cons (cons obj-env meth-env) 
					   (make super)))))))))
	    (make-Object (make (eval class env)))))
        (Send (obj msg args)
	  (let* ((o (eval obj env))
		 (vargs (map (lambda (e) (eval e env)) args)))
	    (variant-case o
	      (Object (class-list)
	        (letrec (find (lambda (l)
			        (if (null? l) (error "msg not understood")
				    (let ((m (lookup (cdr (car l)) msg)))
				      (if m 
					  (eval (cadr m) (extend-list (caar l)
					  (cons 'this (car m)))
					  (cons SOMETHING-FOR-THIS vargs)))))))
		  (find class-list))))))
To bind a value SOMETHING-FOR-THIS to this, we have several choices. Suppose that we have class A a superclass of B, which is a superclass of C.

If we pick o for SOMETHING-FOR-THIS, then the search for a message sent to this within any method of A, B, or C, will begin searching from the actual run-time class of the object. This is called dynamic inheritance.

If we pick (make-Object l) (which is really the same object, since the object environment is shared) for SOMETHING-FOR-THIS, then the search for a message sent to this from class X will start in class X, even if the object has a more specific type (is of a subclass or subsubclass of X). This is called static inheritance.

In C++ you get dynamic inheritance for virtual methods, and static inheritance for non-virtual methods. In Java, dynamic inheritance is used for all methods.

Overriding a superclass method in a subclass hides the name of the superclass method, but you might still like to call that method from subclass methods. To permit this, we bind the name super in a similar manner to binding this. Once again, there seem to be two choices: super could be bound to (make-Object (cdr class-list)) or (make-Object (cdr l)). The first yields dynamic inheritance for super, while the second yields static inheritance for super.

Exercise: figure out how super works in Java, C++, or your favorite object-oriented language.

Now let's consider an example of inheritance. Suppose that color-point inherits from point.

(define point 
  (class (Root) ((netix 0))
	        ((x 0) (y 0))
		(Initialize () body)
		(Draw () body)))

(define color-point
  (class point ()
	       ((color 'white))
	       (Draw () (pixel (Send this Getx) (send this Gety) color))))
Suppose we want to override Move in color-point so that colored points are reset to white before being moved. To do this, we simply reset the color, and send Move to super:
               (Move (mx my) (set! color 'white) (send super Move))
Now suppose we want to add a method OnScreenMove to all points that both moves a point and redraws it. We add this method to point so that colored points will inherit it:
               (OnScreenMove (nx ny) (send this Move) (send this Draw))))
The method OnScreenMove does different things if dynamic inheritance or static inheritance is used, because it invokes different Move and Draw methods.

Implementing Static Inheritance

Compilers attempt to make method lookup fast by making the search process be a simple table index. One common way of doing this is as follows.

Implementing Dynamic Inheritance

Compilers use dispatch tables for dynamic inheritance too. The approach is similar to that for implementing static inheritance. Only one step changes:

Multiple Inheritance

A class may be "like" several other existing classes, so it should inherit from all of them. Suppose C inherits from both A and B. This creates several problems. First: what if a method M is in not in C, but in both A and B? To resolve this name class, Eiffel insists one of the methods be renamed. In C++ the compiler will issue an error indicating an ambiguous reference. Second, if both B and C inherit from A, and D inherits from B and C, how many copies are there of the instance variables of A? Are the replicated or shared? Since Eiffel forces renaming, the instance variables of A are replicated. In C++ the user can specify if there should be two copies (default) or just one (called virtual inheritance).

Multiple inheritance makes creation of the method table difficult. If C inherits from A and B, then C's method table must contain all methods of both A and B. The problem here is that the table can become very large. Consider the following inheritance diagram.

    A     B     D     E
    ^     ^     ^     ^
    |- C -|     |- F -|
       ^           ^
       |---- G ----|
Objects of class G contain complete copies of the dispatch tables for all of A, B, C, D, E, and F.

Static Typing for OO Languages

What is the type of an object? Two answers seem possible: (1) an object is like a record, its type could be a list of instance variables and types and method names and their argument types. (2) an object's type is its class name. The problem with (1) is that types become large and the typing rules are quite complicated. so it is essentially a closure. The problem with (2) is that classes must not first-class values, i.e. they can only be declared at the top level.

Most (all?) object-oriented languages in common use use the second solution. Here are typing rules for send and new using this approach:

A |- e[0] : C    C has M: (t[1] ... t[n] -> t)    A |- e[i] : t[i]
A |- (send e[0] m e[1] ... e[n]) : t

A |- new C : C
Consider (if p (new C) (new D)). If C is a subclass of D, we could let this expression have type D, since C has all the methods of D. This suggests the following sub-classing rule.
A |- P : bool   A |- (new C) : D   A |- (new D) : D
A |- (if P (new C) (new D)) : D
We can not use methods of class C not in D on result of this expression, but this is ok: the result might be a D.

This sub-classing rule leads to multiple inheritance and its attendent problems. Suppose you have classes C and D, with C a subclass of D. You have a class Sortable with methods sort and compare. To use Sortable, you must inherit from it, override compare, and call Sortable.sort. If you want to sort objects of class D, then D must inherit from both C and Sortable.