Inheritance



next up previous contents index
Next: Reference Grammar Up: Implementations Previous: Classes

Inheritance

Inheritance allows a class, called the subclass, to be implemented as an extension of some other class, called the superclass.

Defining Superclasses

A class definition must indicate explicitly that it is available for subclassing by explicitly providing an interface to its subclasses:

        <provides> -> provides <idn_list>
        <hides> -> hides <idn_list>
The provides clause lists private methods, routines, and makers (10.5.2) implemented in the class's module that are available to subclasses; it also lists any public methods where the signature given in the class differs from that given in the type and the class definer wishes this information to be visible to definers of subclasses. Some makers must be provided, since otherwise subclasses will have no way to create new objects of their own; if no maker for the class is listed, there will be a compile-time error. The hides clause lists public methods that are not available to subclasses.

[usage1380]

Makers

A maker is a special operator that fills in the fields of a newly created object:

        <maker_def> -> <maker_interface> <body> end <idn>
        <maker_interface> -> <idn> [<parms>] <formal_args> <makes> [<signals>] [<where>]
        <makes> ->  makes "(" <type_designator> ")"
The final idn in the maker_def must match that given in the maker_interface. The type_designator in the makes clause must denote a class type implemented in the maker's module; we say this is the maker's class.

The object being initialized by a maker is created by the Theta runtime before the maker is called; its class is some subclass of the maker's class, and the new object is an implicit argument of the maker. The maker fills in its fields using a "make" statement; the new object can be referred to explicitly by the name "self", but only within the optional body of the "make" statement (8.17). The usual scoping rules for "self" apply: the instance variables of "self" can also be directly named as variables within the body of the "make" (7.5).

A maker cannot contain a return statement. Normal termination of a make statement causes its execution to terminate normally. If the maker reaches the end of its body, it will terminate with the exception "failure(no return results)".

A maker can be called only within a make statement (8.17) or a class constructor (7.4) within some subclass of the maker's class; the maker cannot be called within its own class. It is used in the subclass to initialize the instance variables inherited from a superclass, and thus its use is limited to modules implementing subclasses of its class. It must be listed in the provides list of its class.

[usage1408]

Subclasses

To inherit code from a superclass, a class includes the inherits clause in its routine_interface:
        <inherits> -> inherits <type_designator> ["{" <renames> ["," <renames>]* "}"]
        <renames> -> <idn> for <idn>
The type_designator names the superclass. The inherits clause makes the superclass name and the names of provided methods and routines visible to code in the subclass's module.

The inheritance hierarchy is independent of the type hierarchy. Therefore the type implemented by the superclass might not be a supertype of the type implemented by the subclass. For example, it might be convenient to implement stacks by inheriting from a class that implements lists even though stack is not a subtype of list. Note also that either class might not implement a type.

The instance variables declared in the subclass are in addition to those of the superclass: objects of the subclass have all the inherited instance variables of the superclass as well as those of the subclass. However, the inherited variables cannot be accessed directly in the subclass; it can access them only by calling the superclass methods.

Objects of the subclass have all the methods of the superclass, although the hidden methods aren't visible within the subclass. Visible superclass methods can be renamed: the second idn in a renames clause gives the name of the method in the superclass; the first gives the new name. For example,

C = class for T inherits D {foo for bar}
renames D's "bar" method to "foo". All superclass methods not mentioned in a renaming clause retain their original names. The effect of the renamings is that the superclass appears to have been rewritten with the names needed in the subclass.

Visible superclass methods can be inherited by the subclass: this is accomplished by simply not giving an implementation of a method of that name. Subclass methods can also be implemented explicitly. If such a method has the same name as a visible superclass method, the new implementation overrides the associated superclass method. In such a case, the subclass object has both the overridden method and the new method; the overridden method is a private method and it can be named using the the special form "" idn. For example, if the subclass overrides visible superclass method "m", the overriding definition is named "m", and the overridden method is named "" [tex2html_wrap2957]m. Thus code in the subclass and its module can continue to call the overridden method using the "" form.

Methods overridden by a subclass can affect the behavior of superclass methods. If a superclass method calls a method "n" that has been overridden by the subclass, the implementation of "n" provided by the subclass will run, not the implementation provided by the superclass. For example, consider superclass method

m ( ) returns (int)
    return (self.n())
end m
and suppose that "n" is visible and has been overridden in the subclass. When "m" is called on an object of the subclass, its call of "n" goes to the overriding definition. Therefore, we require that the overriding definition have a signature that is a subtype of the signature of the method it overrides.

[usage1437]

Within a class-constructor for the subclass, or within a make statement of a maker for the subclass, a maker of the superclass must be called to initialize the superclass fields of the new object. For example, inside a parameterized maker "make_stack"[T], we might have a make statement:

make { ... ; make_list[T](...) }
where stack is being implemented as a subclass of list and "make_list" is a maker provided for list.

A subclass type is not a subtype of the superclass type, nor is it a subtype of the type implemented by the superclass, unless the type implemented by the subclass is a subtype of the type implemented by the superclass. With one exception, ordinary type checking restrictions apply to subclass objects, e.g., a subclass object cannot be assigned to a variable whose type is the superclass type. The exception is that the code of a subclass and its module can call the methods and routines provided by its superclass passing in subclass objects as arguments in positions where an object of the superclass type, or of the type implemented by the superclass, is required.

A subclass can use its hides clause to avoid exporting inherited methods to its subclasses and can use its provides clause to export methods, and also routines and makers implemented in its module. However, it cannot use the "" notation to name methods in the provides clause, and it cannot provide any methods or routines that have the superclass type in their signatures (since its superclass is not visible to its subclasses).

Rules for Superclasses

A superclass should guarantee that subclasses cannot interfere with the correct functioning of its code and the code in its module. This can be accomplished by care in implementing the module and by using the provides and hides clauses appropriately. Below we discuss two problems that must be avoided: masquerading, and propagation of bad information.

None of the methods or routines that the superclass provides to its subclasses should create an alias for one of the superclass objects. If such a method or routine were provided, it could be used by the code in the subclass and its module to cause subclass objects to masquerade as superclass objects. Masquerading is bad because subclass objects may behave differently than superclass objects. For example, if the "bag copy" method were implemented:

        copy ( ) returns (bag[T])
                return (self)
                end copy
it would create an alias for self. If "copy" were provided to subclasses of "bag", "return(x.copy())" within the subclass, where x is an object of the subclass, will cause a subclass object to appear to be a bag object, even though it might not behave like one.

The second problem - propagation of bad information - occurs only if a provided method, routine, or maker violates the superclass rep invariant, and is easily avoided by not providing such violators. However, providing violators is sometimes useful, and they are bad only in combination with propagators: methods, routines, and makers that perform incorrectly if the superclass rep invariant doesn't hold for some object they access. For example, suppose the "bag copy" method simply copied its instance variables; if the rep invariant weren't satisfied, the result would be a "bag" object that did not satisfy the rep invariant. So, a class that provides violators should not also provide propagator methods, routines, and makers to its subclasses. In addition its module should not export to its users any propagator routines, and its other classes should not export any propagator methods.

Example of Inheritance

This section illustrates the use of inheritance by means of a simple example.

Suppose we want to implement the "stack" abstraction specified in Section (9.4) as a subclass of the "bag" implementation given in Section (10.4.3). To do so, at the least we must provide some makers for "stack" to use: a maker for initializing an empty "stack", and also some way of initializing the "stack" returned by the "copy" method. We must also consider how to implement the "top" method.

Here is one solution to these problems: "bag" provides its subclasses with access to the array that contains its elements, e.g., by providing a "get_els" method, which returns the "els" component of a bag. However, this method is effectively a violator, since it allows the subclass to modify the array, thus violating the rep invariant. Therefore care must be taken to not provide any propagators.

module implements create_bag

brep = class[T] for bag[T]
        provides mk_brep, mk_copy, get_els
        hides copy

        sz: int implements size		 	% implementation of size method
        els: array[T] implements get_els	% implementation of get_els method

        % the rep invariant is:  sz = els.size( )

        put (x: T) 
                els.append(x)
                sz := sz + 1
                end put

        get ( ) returns (T) signals (empty)
                x: T := els.remove( ) except when bounds: signal empty end
                sz := sz - 1
                return (x)
                end get

        copy ( ) returns (bag[T])
  	    where T has copy ( ) returns (T)
                return (brep[T]{sz := sz, els := els.copy( )})
                end copy

        end brep

create_bag [T] ( ) returns (bag[T])
        return (brep[T]{sz := 0, els := array_create[T]( )})
        end create_bag

mk_brep[T] ( ) makes (brep[T])
        make {sz := 0, els := array_create[T]( )}
        end mk_brep

mk_copy[T] (x: brep[T]) makes (brep[T])
    where T has copy ( ) returns (T)
        make {sz := x.els.size(), els := x.els.copy( )}
        end mk_copy

end
The "mk_copy" maker ensures the rep invariant by setting the "sz" field of the new object appropriately. The "copy" method is hidden since it is a propagator. If the bag implementation had not exported a violator, it would not be necessary to hide the "copy" method.

Here is an implementation of "stack" that demonstrates the use of inheritance.

module implements create_stack

srep = class[T] for stack[T] inherits brep[T] {push for put, pop for get}

        top ( ) returns (T) signals (empty)
                return (self.get_els( ).top( ))
                        except when bounds: signal empty end	
                end top

        copy ( ) returns (stack[T]) 
 	    where T has copy ( ) returns (T)
                return (srep[T]{mk_copy[T](self)})
                end copy

        end srep

create_stack [T] ( ) returns (stack[T])
        return (srep[T]{mk_brep[T]( )})
        end create_stack
                
end
For this implementation, it is not necessary to define any additional instance variables. Note that the "stack" implementation is dependent on the details of the "bag" implementation, e.g., that "put" adds the new element to the high end of the array and "get" removes the newest element.

In this example, the implemented types ("stack" and "bag") are in a subtype relationship that mirrors the inheritance relationship of the implementing classes, as shown in Figure 10.1. This means that "srep" indirectly provides another implementation of "bag". However, Theta does not require that such a relationship exists; another class could inherit from "brep" without implementing a subtype of "bag".

[figure1474]
Figure 10.1: The subtype and inheritance relationships of srep



next up previous contents index
Next: Reference Grammar Up: Implementations Previous: Classes



theta-questions@lcs.mit.edu