Chapter 11: Polymorphism

 

Keywords: conformance, deferred, effective, dynamic type, dynamic dispatch, polymorphism

A class in an inheritance hierarchy has many types, from the current type to the most abstract type at the top of the hierarchy. A common pattern is to define the feature interface at a high level in the hierarchy, and to defer the body or action of a feature until a later, descendant class. Such a deferred feature has its name and signature defined in a parent, but the definition or body of the feature is given in the child. The child effects or gives an effective definition for the feature, so each child can define its own specific action. Objects of the various child types can be treated identically by calling the feature on the object; the feature interface is identical, but the content of the feature is specific to each class. This technique is called polymorphism, because it allows the same code to use objects of many shapes or types.

 

11.1 The Eiffel type hierarchy

Eiffel uses a class hierarchy to define the very top, and the very bottom levels of any user-defined class. At the top of the Eiffel hierarchy is the class GENERAL, that defines the generic routines clone, copy, and is_equal. Below that is the class PLATFORM, that defines the specific features needed to tailor the Eiffel language to run on a specific platform or computer system, such as the number of bits used to store numbers of type INTEGER, REAL, and DOUBLE. Below that is the class ANY, which is the ancestor of every user-defined class, so all user-defined classes sit below ANY in the hierarchy.

Every class written by a user inherits from the Eiffel class ANY, without the need for an explicit inheritance clause. The class ANY allows features to be defined that work for any class; more formally, that work for all classes of type ANY. System-wide features can be defined once at the appropriate level, and used by all the children of class ANY; in particular, the generic features clone, copy, and is_equal are defined once and used in any class. No class inherits NONE, by definition. Class NONE defines the bottom node in the inheritance hierarchy, so by definition no class can inherit it. The special value Void is of type NONE.

With the introduction of inheritance, what is meant by the type of an object has to be defined in more detail. A worker is a person; more formally, an object of type WORKER is also an object of type PERSON, because WORKER inherits PERSON. The base class of a specific class is defined to be the top-level user class, at the top of the user-defined hierarchy. The inheritance hierarchy for a class may be many levels deep, so a class can be of many types, from its current type all the way up to type ANY. In the previous chapter, an inheritance hierarchy was defined for a CONTRACTOR, who is a WORKER, who is a PERSON, who is of type ANY. This reflects the real world: I am a worker, a person, a human being, an animal, a living being, and a thing (in some languages, the top level of the inheritance hierarchy is a class THING). An object that is an instance of a class in an inheritance hierarchy is of many types. A class has an immediate type, as well as one or more inherited types. A generic class can generate many types, one type for each actual parameter.

Given the Eiffel class hierarchy, in particular the two classes ANY and NONE, it can now be seen that there is a single, consistent rule for export policies; a feature is exported to the classes listed in its export policy. A feature exported to objects of type ANY is thus available to all classes; a feature exported to objects of type NONE is available to no classes. A feature exported to one or more specific classes is available to those classes, and to descendants of those classes, because a descendant of class X is of type X, as well as more specific types.

 

11.2 Conformance

Inheritance in the Eiffel type system provides a way to define one type in terms of another. It also determines when one type can replace another, and ensures that the type system works in an intuitively reasonable manner. Consider an example where you go into a restaurant and ask for a salad. For this request, it is reasonable that any type of salad will be acceptable, such as a garden salad, a Waldorf salad, a chef's salad, and so on. These are specific types of salad, so they conform to the definition of a salad. On the other hand, receiving a hamburger would be a surprise, because a hamburger is not a type of salad. The notion of conformance makes this expectation explicit.

In an assignment statement, the type of the right hand side must be the same as the type of the variable on the left hand side. Because a variable may have many types due to inheritance, a more formal rule must be given: the type of the expression on the right of the assignment must conform to the type on the left. Class B conforms to class A if they are the same class, or class B is a descendant of A; A cannot also conform to B, unless they are the same type.

Consider the classes CONTRACTOR, WORKER, and PERSON, a hierarchy of three classes. A contractor is a type of worker, and a worker is a type of person. If I need a job done and advertise for the services of a worker, then there is no surprise if I use a contractor, because a contractor can take the place of a worker. On the other hand, I would be surprised if a person, who is not a worker, answered the ad; I want a more specific class. In the same way, an Eiffel variable can treat an object of a parent type in the same way it treats an object of a subtype, because the child does at least as much as the parent, and possibly more. One type can be used in place of another if they conform.
 

    p: PERSON p := c -- valid; a contractor is a person
    w: WORKER p := w -- valid; a worker is a person
    c: CONTRACTOR w := p -- invalid; a person is not a worker
    w := c -- valid; a contractor is a worker
    c := p -- invalid; a person is not a contractor
    c := w -- invalid; a worker is not a contractor
 

When a feature is redefined, the signature of the new version must conform to the signature of its precursor. The signature of a feature lists the number, order and type of the values passed as arguments to the routine, and the type of any value returned from the routine. To ensure that redefinition works in a reasonable manner, the signatures of any old and new versions of a feature must conform; formally, each type in the signature of the new version must conform to the type in the old version. A redefined feature often keeps the original signature; conformance allows descendants to replace their ancestors in the signature.

An expanded type conforms directly to its base type, and indirectly to other classes through the base type. Consider the two declarations and the assignment

 
    ref_type: T
    exp_type: expanded T
    ref_type := exp_type
 

This assignment is valid, and has the effect of copying the values of exp_type into the variable ref_type. Expanded types are discussed in more detail in Meyer (1992).

 

11.3 Deferred features

The foundation of reusable software is to define a feature once, and use the feature as needed. One of the most powerful inheritance techniques is to define the feature interface for a general class of objects in a general class, and then leave each specific, inheriting class to define its own specific action or body of the feature. Every child plays the same role and has similar behaviour, but the exact, internal details of the behaviour are different.

The routine header defines the interface to the routine, and the routine body defines the action. A routine may be defined with a header, but no body; such a routine cannot be executed. The interface is defined in a parent class, and the body is deferred; a child class then inherits the routine, and defines the body. The parent defines a deferred routine, and the child makes this routine effective; such a process is called effecting the routine.

A deferred routine is defined by replacing the keyword do with the keyword deferred, and leaving the body of the routine empty. A deferred routine to find an area, for example, is

deferred class X

feature

    area: REAL is

                -- area

    deferred

end

A class with a deferred routine is called a deferred class; this is stated in the class header. An instance of a deferred class cannot be created, because Eiffel cannot find a routine body to connect to the header. A deferred class therefore does not contain a creation clause. An instance of a child class can be created if the child effects every deferred routine, so there are no deferred routines in the child.

Common error: forget to state that class is deferred, or forget to effect a feature

Error code: VCCH (1)

Error: Class has deferred feature(s), but is not declared as deferred.

What to do: make feature(s) effective, or include "deferred" before "class" in Class_header

Common error: Try to create an object of a deferred type. Either you need to make the deferred class effective by effecting every deferred feature, or you need to use an effective child of the deferred class.

Error code: VGCC (2)

Type error: creation instruction applies to target of a deferred type

What to do: make sure that type of target is effective.

 

11.4 A deferred example: class POLYGON

Consider a system that implements a graphics library. Classes in the library define geometric shapes such as points, lines, circles, triangles, squares, and rectangles. A polygon is a general name for closed geometric objects made of straight lines, such as triangles and squares. An abstract class POLYGON may therefore be defined to capture the general properties and behaviour of polygons; this class is then inherited by specific types of polygon.

Operations on polygons include computing the area and perimeter of a shape, moving the shape around, or changing the size of the shape. These behaviours can be defined in the abstract class POLYGON as effective or as deferred routines. A polygon has a perimeter, and the length of this perimeter can be easily calculated for arbitrary polygons, so an effective routine to calculate the perimeter is defined in this class. A polygon has an area, but it is difficult to define a method for finding the area of an arbitrary polygon, so the body of this feature is deferred. Defining the function area as a deferred routine says that all polygons have an area, but the effective routine to calculate the area is left for more specific classes to define.

The code for the deferred class POLYGON looks like

deferred class POLYGON

feature {NONE}

    vertices: LINKED_LIST[POINT]

feature {ANY}

  make is
                -- get the points that define the polygon
        deferred

        end -- make

  perimeter: REAL is
                -- the length of the perimeter of the polygon
 
       local this, previous: POINT

        do
                from
                vertices.start
                this := vertices.item
                until vertices.islast
                loop
                    previous := this
                    vertices.forth
                    this := vertices.item
                    Result := Result + this.distance (previous)
                end
                Result := Result + this.distance (vertices.first)
        end -- perimeter
 
  area: REAL is
                -- return the area of the figure
 
       deferred
        end -- area

 display is
                -- display the location of the vertices
 
        do
               from vertices.start
               until vertices.after
                  loop
                     vertices.item.display
                     vertices.forth
                 end

       end -- display

  move (delta_x, delta_y: REAL) is
                -- move by delta_x horizontally and delta_y vertically

        do
               from vertices.start
               until vertices.after
              loop
                  vertices.item.move (delta_x, delta_y)
                  vertices.forth
              end
    end -- move

end -- class POLYGON

Specific types of polygon, such as triangles, rectangles and squares, define effective versions for each deferred routine. A deferred routine is not redefined in the child, because it was never defined; the child defines an effective routine. An effective parent routine may be used by the child as written, or they may be redefined to more specific versions.

 

11.5 An effective example: class RECTANGLE

A rectangle is a polygon with four sides, where the sides meet at right angles. Rectangles are created, moved, and displayed like any other polygon, and have an area and perimeter. On the other hand, a rectangle has special features of its own (matching sides, four vertices, right angles) which may result in better ways to do some of the operations. RECTANGLE can thus be defined as a child of POLYGON, the inherited features can be effected or changed, and new features can be added as needed. To create a rectangle, all the RECTANGLE routines have to be effective, because it is impossible to create an object of a deferred type. One possible way to implement the class RECTANGLE is

class RECTANGLE

inherit
    POLYGON

redefine perimeter

end

creation
    make

feature { NONE}

    number_of_vertices: INTEGER is 4
    side1, side2: REAL

feature {ANY}

  make is
                -- make a rectangle, store the lengths of the sides
        local
        i: INTEGER
        p: POINT
        do
                !!vertices.make
                io.putstring ("%NEnter the four points of the rectangle")
                from
                until i = number_of_vertices
                loop
                    !!p.make
                    vertices.extend (p)
          &nb