ET: Inheritance

Inheritance is a powerful and attractive technique. A look at either the practice or literature shows, however, that it is not always well applied. Eiffel has made a particular effort to tame inheritance for the benefit of modelers and software developers. Many of the techniques are original with Eiffel. Paul Dubois has written (comp.lang.python Usenet newsgroup, 23 March 1997): there are two things that [Eiffel] got right that nobody else got right anywhere else: support for design by contract, and multiple inheritance. Everyone should understand these "correct answers" if only to understand how to work around the limitations in other languages.

Basic inheritance structure

To make a class inherit from another, simply use an inherit clause: note ... class D inherit A B ... feature ...

This makes D an heir of A, B and any other class listed. Eiffel supports multiple inheritance: a class may have as many parents as it needs. Later sections ( "Multiple inheritance and renaming" and "Repeated inheritance and selection" ) will explain how to handle possible conflicts between parent features.

Note: This discussion will rely on the terminology introduced in The Static Picture: System Organization: descendants of a class are the class itself, its heirs, the heirs of its heirs and so on. Proper descendants exclude the class itself. The reverse notions are ancestors and proper ancestors.

By default D will simply include all the original features of A, B, ..., to which it may add its own through its feature clauses if any. But the inheritance mechanism is more flexible, allowing D to adapt the inherited features in many ways. Each parent name -- A, B, ... in the example -- can be followed by a Feature Adaptation clause, with subclauses, all optional, introduced by keywords rename, export, undefine, redefine and select, enabling the author of D to make the best use of the inheritance mechanism by tuning the inherited features to the precise needs of D. This makes inheritance a principal tool in the Eiffel process, mentioned earlier, of carefully crafting each individual class, like a machine, for the benefit of its clients. The next sections review the various Feature Adaptation subclauses.

Redefinition

The first form of feature adaptation is the ability to change the implementation of an inherited feature.

Assume a class SAVINGS_ACCOUNT that specializes the notion of account. It is probably appropriate to define it as an heir to class ACCOUNT, to benefit from all the features of ACCOUNT still applicable to savings accounts, and to reflect the conceptual relationship between the two types: every savings account, apart from its own specific properties, also "is" an account. But we may need to produce a different effect for procedure deposit which, besides recording the deposit and updating the balance, may also need, for example, to update the interest.

This example is typical of the form of reuse promoted by inheritance and crucial to effective reusability in software: the case of reuse with adaptation. Traditional forms of reuse are all-or-nothing: either you take a component exactly as it is, or you build your own. Inheritance will get us out of this "reuse or redo" dilemma by allowing us to reuse and redo. The mechanism is feature redefinition: note description: "Savings accounts" class SAVINGS_ACCOUNT inherit ACCOUNT redefine deposit end feature -- Element change deposit (sum: INTEGER) -- Add sum to account. do ... New implementation (see below) ... end ... Other features ... end -- class SAVINGS_ACCOUNT

Without the redefine subclause, the declaration of deposit would be invalid, yielding two features of the same name, the inherited one and the new one. The subclause makes this valid by specifying that the new declaration will override the old one.

In a redefinition, the original version -- such as the ACCOUNT implementation of deposit in this example -- is called the precursor of the new version. It is common for a redefinition to rely on the precursor's algorithm and add some other actions; the reserved word Precursor helps achieve this goal simply. Permitted only in a routine redefinition, it denotes the parent routine being redefined. So here the body of the new deposit (called "New implementation" above) could be of the form

Precursor (sum) -- Apply ACCOUNT's version of deposit ... Instructions to update the interest ...

In the event that a routine has redefined a particular feature from multiple parent, the Precursor syntax allows the inclusion of a parent qualification:

Precursor {PARENT_X} (args...) -- Apply PARENT_X's version of this feature ... Instructions to update the interest ...

Besides changing the implementation of a routine, a redefinition can turn an argument-less function into an attribute; for example a proper descendant of ACCOUNT could redefine deposits_count, originally a function, as an attribute. The Uniform Access Principle (introduced in The Dynamic Structure: Execution Model ) guarantees that the redefinition makes no change for clients, which will continue to use the feature under the form acc.deposits_count

Polymorphism

The inheritance mechanism is relevant to both roles of classes: module and type. Its application as a mechanism to reuse, adapt and extend features from one class to another, as just seen, covers its role as a module extension mechanism. But it's also a subtyping mechanism. To say that D is an heir of A, or more generally a descendant of A, is to express that instances of D can be viewed as instances of A.

Polymorphic assignment supports this second role. In an assignment x := y, the types of x and y do not have, with inheritance, to be identical; the rule is that the type of y must simply conform to the type of x.

Definition -- Conformance: A class D conforms to a class A if and only if it is a descendant of A (which includes the case in which A and D are the same class); if these classes are generic, conformance of D [U] to C [T] requires in addition that type U conform to type T (through the recursive application of the same rules).

Note: In addition, it will be shown in the discussion of tuples ("Tuple types"), that TUPLE [X] conforms to TUPLE, TUPLE [X, Y] to TUPLE [X] and so on.

So with the inheritance structure that we have seen, the declarations acc: ACCOUNT sav: SAVINGS_ACCOUNT

make it valid to write the assignment acc := sav

which will assign to acc a reference attached (if not void) to a direct instance of type SAVINGS_ACCOUNT, not ACCOUNT.

Such an assignment, where the source and target types are different, is said to be polymorphic. An entity such as acc, which as a result of such assignments may become attached at run time to objects of types other than the one declared for it, is itself called a polymorphic entity.

For polymorphism to respect the reliability requirements of Eiffel, it must be controlled by the type system and enable static type checking. We certainly do not want an entity of type ACCOUNT to become attached to an object of type DEPOSIT. Hence the second typing rule:

Rule -- Type Conformance: An assignment x := y, or the use of y as actual argument corresponding to the formal argument x in a routine call, is only valid if the type of y conforms to the the type of x.

The second case listed in the rule is a call such as target.routine(..., y, ...) where the routine declaration is of the form routine (..., x: SOME_TYPE). The relationship between y, the actual argument in the call, and the corresponding formal argument x, is exactly the same as in an assignment x := y: not just the type rule, as expressed by Type Conformance (the type of y must conform to SOME_TYPE), but also the actual run-time effect which, as for assignments, will be either a reference attachment or, for expanded types, a copy.

The ability to accept the assignment x := Void for x of any reference type (see "Basic operations" ) is a consequence of the Type Conformance rule, since Void is of type NONE which by construction ("The global inheritance structure" ) conforms to all types.

Polymorphism also yields a more precise definition of "instance". A direct instance of a type A is an object created from the exact pattern defined by the declaration of A 's base class, with one field for each of the class attributes; you will obtain it through a creation instruction of the form create x ..., for x of type A, or by cloning an existing direct instance. An instance of A is a direct instance of any type conforming to A: A itself, but also any type based on descendant classes. So an instance of SAVINGS_ACCOUNT is also an instance, although not a direct instance, of ACCOUNT.

A consequence of polymorphism is the ability to define polymorphic data structures. With a declaration such as accounts: LIST [ACCOUNT]

the procedure call accounts.extend (acc), because it uses a procedure extend which in this case expects an argument of any type conforming to ACCOUNT, will be valid not only if acc is of type ACCOUNT but also if it is of a descendant type such as SAVINGS_ACCOUNT. Successive calls of this kind make it possible to construct a data structure that, at run-time, might contain objects of several types, all conforming to ACCOUNT:

Such polymorphic data structures combine the flexibility and safety of genericity and inheritance. You can make them more or less general by choosing for the actual generic parameter, here ACCOUNT, a type higher or lower in the inheritance hierarchy. Static typing is again essential here, prohibiting for example a mistaken insertion of the form accounts.extend (dep) where dep is of type DEPOSIT, which does not conform to ACCOUNT.

At the higher (most abstract) end of the spectrum, you can produce an unrestrictedly polymorphic data structure general_list: LIST [ANY] which makes the call general_list.extend (x) valid for any x. The price to pay is that retrieving an element from such a structure will yield an object on which the only known applicable operations are the most general ones, valid for all types: assignment, copy, twin, equality comparison and others from ANY. The object test, studied below, will make it possible to apply more specific operations after checking dynamically that a retrieved object is of the appropriate type.

Dynamic binding

The complement of polymorphism is dynamic binding, the answer to the question "What version of a feature will be applied in a call whose target is polymorphic?".

Consider acc is of type ACCOUNT. Thanks to polymorphism, an object attached to acc may be a direct instance not just of ACCOUNT but also of SAVINGS_ACCOUNT or other descendants. Some of these descendants, indeed SAVINGS_ACCOUNT among them, redefine features such as deposit. Then we have to ask what the effect will be for a call of the form acc.deposit (some_value)

Dynamic binding is the clearly correct answer: the call will execute the version of deposit from the generating class of the object attached to acc at run time. If acc is attached to a direct instance of ACCOUNT, execution will use the original ACCOUNT version; if acc is attached to a direct instance of SAVINGS_ACCOUNT, the call will execute the version redefined in that class.

This is a clear correctness requirement. A policy of static binding (as available for example in C++ or Delphi, for non-virtual functions) would take the declaration of acc as an ACCOUNT literally. But that declaration is only meant to ensure generality, to enable the use of a single entity acc in many different cases: what counts at execution time is the object that acc represents. Applying the ACCOUNT version to a SAVINGS_ACCOUNT object would be wrong, possibly leading in particular to objects that violate the invariant of their own generating class (since there is no reason a routine of ACCOUNT will preserve the specific invariant of a proper descendant such as SAVINGS_ACCOUNT, which it does not even know about).

In some cases, the choice between static and dynamic binding does not matter: this is the case for example if a call's target is not polymorphic, or if the feature of the call is redefined nowhere in the system. In such cases the use of static binding permits slightly faster calls (since the feature is known at compile time). This application of static binding should, however, be treated as a compiler optimization. The EiffelStudio compiler, under its "finalization" mode, which performs extensive optimization, will detect some of these cases and process them accordingly -- unlike approaches that make developers responsible for specifying what should be static and what dynamic (a tedious and error-prone task, especially delicate because a minute change in the software can make a static call, in a far-away module of a large system, suddenly become dynamic). Eiffel programmers don't need to worry about such aspects; they can rely on the semantics of dynamic binding in all cases, with the knowledge that the compiler will apply static binding when safe and desirable.

Even in cases that require dynamic binding, the design of Eiffel, in particular the typing rules, enable compilers to make the penalty over the static-binding calls of traditional approaches very small and, most importantly, constant-bounded : it does not grow with the depth or complexity of the inheritance structure. The discovery in 1985 of a technique for constant-time dynamic binding calls, even in the presence of multiple and repeated inheritance, was the event that gave the green light to the development of Eiffel.

Dynamic binding is particularly interesting for polymorphic data structures. If you iterate over the list of accounts of various kinds, accounts: LIST [ACCOUNT], illustrated in the last figure, and at each step let acc represent the current list element, you can repeatedly apply acc.deposit (...)

to have the appropriate variant of the deposit operation triggered for each element.

The benefit of such techniques appears clearly if we compare them with the traditional way to address such needs: using multi-branch discriminating instructions of the form if "Account is a savings account " then ... elseif "It is a money market account" then ... elseif ... and so on, or the corresponding case ... of ..., switch or inspect instructions. Apart from their heaviness and complexity, such solutions cause many components of a software system to rely on the knowledge of the exact set of variants available for a certain notion, such as bank account. Then any addition, change or removal of variants can cause a ripple of changes throughout the architecture. This is one of the majors obstacles to extendibility and reusability in traditional approaches. In contrast, using the combination of inheritance, redefinition, polymorphism and dynamic binding makes it possible to have a point of single choice -- a unique location in the system which knows the exhaustive list of variants. Every client then manipulates entities of the most general type, ACCOUNT, through dynamically bound calls of the form acc.some_account_feature (...)

These observations make dynamic binding appear for what it is: not an implementation mechanism, but an architectural technique that plays a key role (along with information hiding, which it extends, and Design by Contract, to which it is linked through the assertion redefinition rules seen below) in providing the modular system architectures of Eiffel, the basis for the method's approach to reusability and extendibility. These properties apply as early as analysis and modeling, and continue to be useful throughout the subsequent steps.

Deferred features and classes

The examples of dynamic binding seen so far assumed that all classes were fully implemented, and dynamically bound features had a version in every relevant class, including the most general ones such as ACCOUNT.

It is also useful to define classes that leave the implementation of some of their features entirely to proper descendants. Such an abstract class is known as deferred; so are its unimplemented features. The reverse of deferred is effective, meaning fully implemented.

LIST is a typical example of deferred class. As it describes the general notion of list, it should not favor any particular implementation; that will be the task of its effective descendants, such as LINKED_LIST (linked implementation), TWO_WAY_LIST (linked both ways) ARRAYED_LIST, (implementation by an array), all effective, and all indeed to be found in EiffelBase.

At the level of the deferred class LIST, some features such as extend (add an item at the end of the list) will have no implementation and hence will be declared as deferred. Here is the corresponding form, illustrating the syntax for both deferred classes and their deferred features: note description: "[ Sequential finite lists, without a commitment to a representation. ]" deferred class LIST [G] feature -- Access count: INTEGER -- Number of items in list do ... See below; this feature can be effective ... end feature -- Element change extend (x: G) -- Add `x' at end of list. require space_available: not full deferred ensure one_more: count = old count + 1 end ... Other feature declarations and invariants ... end -- class LIST

A deferred feature (considered to be a routine, although it can yield an attribute in a proper descendant) has the single keyword deferred in lieu of the do Instructions clause of an effective routine. A deferred class -- defined as a class that has at least one deferred feature -- must be introduced by deferred class instead of just class.

As the example of extend shows, a deferred feature, although it has no implementation, can be equipped with assertions. They will be binding on implementations in descendants, in a way to be explained below.

Deferred classes do not have to be fully deferred. They may contain some effective features along with their deferred ones. Here, for example, we may express count as a function: count: INTEGER -- Number of items in list do from start until after loop Result := Result + 1 forth end end

This implementation relies on the loop construct described below ( from introduces the loop initialization) and on a set of deferred features of the class which allow traversal of a list based on moving a fictitious cursor: start to bring the cursor to the first element if any, after to find out whether all relevant elements have been seen, and forth (with precondition not after) to advance the cursor to the next element. Procedure forth itself appears as forth -- Advance cursor by one position require not_after: not after deferred ensure moved_right: index = old index + 1 end

where index -- another deferred feature -- is the integer position of the cursor.

Although the above version of feature count is time-consuming -- it implies a whole traversal just for the purpose of determining the number of elements -- it has the advantage of being applicable to all variants, without any commitment to a choice of implementation, as would follow for example if we decided to treat count as an attribute. Proper descendants can always redefine count for more efficiency.

Function count illustrates one of the most important contributions of the method to reusability: the ability to define behavior classes that capture common behaviors (such as count) while leaving the details of the behaviors (such as start, after, forth) open to many variants. As noted earlier, traditional approaches to reusability provide closed reusable components. A component such as LIST, although equipped with directly usable behaviors such as count, is open to many variations, to be provided by proper descendants.

Note: Some O-O languages support only the two extremes: fully effective classes, and fully deferred "interfaces", but not classes with a mix of effective and deferred features. This is an unacceptable limitation, negating the object-oriented method's support for a seamless, continuous spectrum from the most abstract to the most concrete.

A class B inheriting from a deferred class A may provide implementations -- effective declarations -- for the features inherited in deferred form. In this case there is no need for a redefine subclause; the effective versions simply replace the inherited versions. The class is said to effect the corresponding features. If after this process there remain any deferred features, B is still considered deferred, even if it introduces no deferred features of its own, and must be declared as class deferred.

In the example, classes such as LINKED_LIST and ARRAYED_LIST will effect all the deferred features they inherit from LIST -- extend, start etc. -- and hence will be effective.

Except in some applications restricted to pure system modeling -- as discussed next -- the main benefit of deferred classes and features comes from polymorphism and dynamic binding. Because extend has no implementation in class LIST, a call of the form my_list.extend(...) with my_list of type LIST [T] for some T can only be executed if my_list is attached to a direct instance of an effective proper descendant of LIST, such as LINKED_LIST; then it will use the corresponding version of extend. Static binding would not even make sense here.

Even an effective feature of LIST such as count may depend on deferred features (start and so on), so that a call of the form my_list.count can only be executed in the context of an effective descendant.

All this indicates that a deferred class must have no direct instance. (It will have instances, the direct instances of its effective descendants.) If it had any, we could call deferred features on them, leading to execution-time impossibility. The rule that achieves this goal is simple: if the base type of x is a deferred class, no creation instruction of target x, of the form create x..., is permitted.

Applications of deferred classes

Deferred classes cover abstract notions with many possible variants. They are widely used in Eiffel where they cover various needs:

  • Capturing high-level classes, with common behaviors.
  • Defining the higher levels of a general taxonomy, especially in the inheritance structure of a library.
  • Defining the components of an architecture during system design, without commitment to a final implementation.
  • Describing domain-specific concepts in analysis and modeling.

These applications make deferred classes a central tool of the Eiffel method's support for seamlessness and reversibility. The last one in particular uses deferred classes and features to model objects from an application domain, without any commitment to implementation, design, or even software (and computers). Deferred classes are the ideal tool here: they express the properties of the domain's abstractions, without any temptation of implementation bias, yet with the precision afforded by type declarations, inheritance structures (to record classifications of the domain concepts), and contracts to express the abstract properties of the objects being described.

Rather than using a separate method and notation for analysis and design, this approach integrates seamlessly with the subsequent phases (assuming the decision is indeed taken to develop a software system): it suffices to refine the deferred classes progressively by introducing effective elements, either by modifying the classes themselves, or by introducing design- and implementation-oriented descendants. In the resulting system, the classes that played an important role for analysis, and are the most meaningful for customers, will remain important; as we have seen ( "Seamlessness and reversibility" ) this direct mapping property is a great help for extendibility.

The following sketch (from the book Object-Oriented Software Construction, 2nd Edition ) illustrates these ideas on the example of scheduling the programs of a TV station. This is pure modeling of an application domain; no computers or software are involved yet. The class describes the notion of program segment.

Note the use of assertions to define semantic properties of the class, its instances and its features. Although often presented as high-level, most object-oriented analysis methods (with the exception of Walden's and Nerson's Business Object Notation) have no support for the expression of such properties, limiting themselves instead to the description of broad structural relationships. note description: "Individual fragments of a broadcasting schedule" deferred class SEGMENT feature -- Access schedule: SCHEDULE deferred end -- Schedule to which segment belongs index: INTEGER deferred end -- Position of segment in its schedule starting_time, ending_time: INTEGER deferred end -- Beginning and end of scheduled air time next: SEGMENT deferred end -- Segment to be played next, if any sponsor: COMPANY deferred end -- Segment's principal sponsor rating: INTEGER deferred end -- Segment's rating (for children's viewing etc.) Minimum_duration: INTEGER = 30 -- Minimum length of segments, in seconds Maximum_interval: INTEGER = 2 -- Maximum time (seconds) between successive segments feature -- Element change set_sponsor (s: SPONSOR) require not_void: s /= Void deferred ensure sponsor_set: sponsor = s end ... change_next, set_rating omitted ... invariant in_list: (1 <= index) and (index <= schedule.segments.count) in_schedule: schedule.segments.item (index) = Current next_in_list: (next /= Void) implies (schedule.segments.item (index + 1) = next) no_next_if_last: (next = Void) = (index = schedule.segments.count) non_negative_rating: rating >= 0 positive times: (starting_time > 0) and (ending_time > 0) sufficient_duration: ending_time - starting_time >= Minimum_duration decent_interval: (next.starting_time) - ending_time <= Maximum_interval end

Structural property classes

Some deferred classes describe a structural property, useful to the description of many other classes. Typical examples are classes of the Kernel Library in EiffelBase:

NUMERIC describes objects on which arithmetic operations +, -, *, / are available, with the properties of a ring (associativity, distributivity, zero elements etc.). Kernel Library classes such as INTEGER and REAL -- but not, for example, STRING -- are descendants of NUMERIC. An application that defines a class MATRIX may also make it a descendant of NUMERIC.

COMPARABLE describes objects on which comparison operations <, <=, >, >= are available, with the properties of a total preorder (transitivity, irreflexivity). Kernel Library classes such as CHARACTER, STRING and INTEGER -- but not our MATRIX example -- are descendants of COMPARABLE.

For such classes it is again essential to permit effective features in a deferred class, and to include assertions. For example class COMPARABLE declares infix "<" as deferred, and expresses >, >= and <= effectively in terms of it.

Note: The type like Current will be explained in "Covariance, anchored declarations, and "catcalls"" ; you may understand it, in the following class, as equivalent to COMPARABLE.

note description: "Objects that can be compared according to a total preorder relation" deferred class COMPARABLE feature -- Comparison infix "<" (other: like Current): BOOLEAN -- Is current object less than `other'? require other_exists: other /= Void deferred ensure asymmetric: Result implies not (other < Current) end infix "<=" (other: like Current): BOOLEAN -- Is current object less than or equal to `other'? require other_exists: other /= Void do Result := (Current < other) or is_equal (other) ensure definition: Result = (Current < other) or is_equal (other) end ... Other features: infix ">", min, max, ... invariant irreflexive: not (Current < Current) end -- class COMPARABLE

Multiple inheritance and renaming

It is often necessary to define a new class in terms of several existing ones. For example:

The Kernel Library classes INTEGER and REAL must inherit from both NUMERIC and COMPARABLE.

A class TENNIS_PLAYER, in a system for keeping track of player ranking, will inherit from COMPARABLE, as well as from other domain-specific classes.

A class COMPANY_PLANE may inherit from both PLANE and ASSET.

Class ARRAYED_LIST, describing an implementation of lists through arrays, may inherit from both LIST and ARRAY.

In all such cases multiple inheritance provides the answer.

Multiple inheritance can cause name clashes : two parents may include a feature with the same name. This would conflict with the ban on name overloading within a class -- the rule that no two features of a class may have the same name. Eiffel provides a simple way to remove the name clash at the point of inheritance through the rename subclause, as in note description: "Sequential finite lists implemented as arrays" class ARRAYED_LIST [G] inherit LIST [G] ARRAY [G] rename count as capacity, item as array_item end feature ... end -- class ARRAYED_LIST

Here both LIST and ARRAY have features called count and item. To make the new class valid, we give new names to the features inherited from ARRAY, which will be known within ARRAYED_LIST as capacity and array_item. Of course we could have renamed the LIST versions instead, or renamed along both inheritance branches.

Every feature of a class has a final name : for a feature introduced in the class itself ("immediate" feature) it is the name appearing in the declaration; for an inherited feature that is not renamed, it is the feature's name in the parent; for a renamed feature, it is the name resulting from the renaming. This definition yields a precise statement of the rule against in-class overloading:

Rule -- Final Name: Two different features of a class may not have the same final name.

It is interesting to compare renaming and redefinition. The principal distinction is between features and feature names. Renaming keeps a feature, but changes its name. Redefinition keeps the name, but changes the feature. In some cases, it is of course appropriate to do both.

Renaming is interesting even in the absence of name clashes. A class may inherit from a parent a feature which it finds useful for its purposes, but whose name, appropriate for the context of the parent, is not consistent with the context of the heir. This is the case with ARRAY's feature count in the last example: the feature that defines the number of items in an array -- the total number of available entries -- becomes, for an arrayed list, the maximum number of list items; the truly interesting indication of the number of items is the count of how many items have been inserted in the list, as given by feature count from LIST. But even if we did not have a name clash because of the two inherited count features we should rename ARRAY 's count as capacity to maintain the consistency of the local feature terminology.

The rename subclause appears before all the other feature adaptation subclauses -- redefine already seen, and the remaining ones export, undefine and select -- since an inherited feature that has been renamed sheds its earlier identity once and for all: within the class, and to its own clients and descendants, it will be known solely through the new name. The original name has simply disappeared from the name space. This is essential to the view of classes presented earlier: self-contained, consistent abstractions prepared carefully for the greatest enjoyment of clients and descendants.

Inheritance and contracts

A proper understanding of inheritance requires looking at the mechanism in the framework of Design by Contract, where it will appear as a form of subcontracting.

The first rule is that invariants accumulate down an inheritance structure:

Rule -- Invariant Accumulation: The invariants of all the parents of a class apply to the class itself.

The invariant of a class is automatically considered to include -- in the sense of logical "and" -- the invariants of all its parents. This is a consequence of the view of inheritance as an "is" relation: if we may consider every instance of B as an instance of A, then every consistency constraint on instances of A must also apply to instances of B.

Next we consider routine preconditions and postconditions. The rule here will follow from an examination of what contracts mean in the presence of polymorphism and dynamic binding.

Consider a parent A and a proper descendant B (a direct heir on the following figure), which redefines a routine r inherited from A.

As a result of dynamic binding, a call a1.r from a client C may be serviced not by A's version of r but by B 's version if a1, although declared of type A, becomes at run time attached to an instance of B. This shows the combination of inheritance, redefinition, polymorphism and dynamic binding as providing a form of subcontracting; A subcontracts certain calls to B.

The problem is to keep subcontractors honest. Assuming preconditions and postconditions as shown on the last figure, a call in C of the form if a1.pre then a1.r end

or possibly a1.q a1.r

where the postcondition of some routine q implies the precondition pre of r, satisfies the terms of the contract and hence is entitled to being handled correctly -- to terminate in a state satisfying a1.post. But if we let the subcontractor B redefine the assertions to arbitrary pre' and post', this is not necessarily the case: pre' could be stronger than pre, enabling B not to process correctly certain calls that are correct from A's perspective; and post' could be weaker than post, enabling B to do less of a job than advertized for r in the Contract Form of A, the only official reference for authors of client classes such as C. (An assertion p is stronger than or equal to an assertion q if p implies q in the sense of boolean implication.)

The rule, then, is that for the redefinition to be correct the new precondition pre' must be weaker than or equal to the original pre, and the new postcondition post' must be stronger than or equal to the original post.

Because it is impossible to check simply that an assertion is weaker or stronger than another, the language rule relies on different forms of the assertion constructs, require else and ensure then, for redeclared routines. They rely on the mathematical property that for any assertions p and q, the following are true: 1) p implies (p or q) 2) (p and q) implies pFor a precondition, using require else with a new assertion will perform an or, which can only weaken the original; for a postcondition, ensure then will perform an and, which can only strengthen the original. Hence the rule:

Rule -- Assertion Redeclaration: In the redeclared version of a routine, it is not permitted to use a require or ensure clause. Instead you may: Introduce a new condition with require else, for or-ing with the original precondition. Introduce a new condition with ensure then, for and-ing with the original postcondition. In the absence of such a clause, the original assertions are retained.

The last case -- retaining the original -- is frequent but by no means universal.

The Assertion Redeclaration rule applies to redeclarations. This terms covers not just redefinition but also effecting (the implementation, by a class, of a feature that it inherits deferred). The rules -- not just for assertions but also, as reviewed below, for typing -- are indeed the same in both cases. Without the Assertion Redeclaration rule, assertions on deferred features, such as those on extend, count and forth in "Deferred features and classes" , would be almost useless -- wishful thinking; the rule makes them binding on all effectings in descendants.

From the Assertion Redeclaration rule follows an interesting technique: abstract preconditions. What needs to be weakened for a precondition (or strengthened for a postcondition) is not the assertion's concrete semantics but its abstract specification as seen by the client. A descendant can change the implementation of that specification as it pleases, even to the effect of strengthening the concrete precondition, as long as the abstract form is kept or weakened. The precondition of procedure extend in the deferred class LIST provided an example. We wrote the routine (in "Deferred features and classes" ) as extend (x: G) -- Add `x' at end of list. require space_available: not full deferred ensure one_more: count = old count + 1 end

The precondition expresses that it is only possible to add an item to a list if the representation is not full. We may well consider -- in line with the Eiffel principle that whenever possible structures should be of unbounded capacity -- that LIST should by default make full always return false: full: BOOLEAN -- Is representation full? -- (Default: no) do Result := False end

Now a class BOUNDED_LIST that implements bounded-size lists (inheriting, like the earlier ARRAYED_LIST, from both LIST and ARRAY) may redefine full: full: BOOLEAN -- Is representation full? -- (Answer: if and only if number of items is capacity) do Result := (count = capacity) end

Procedure extend remains applicable as before; any client that used it properly with LIST can rely polymorphically on the FIXED_LIST implementation. The abstract precondition of extend has not changed, even though the concrete implementation of that precondition has in fact been strengthened.

Note that a class such as BOUNDED_LIST, the likes of which indeed appear in EiffelBase, is not a violation of the Eiffel advice to stay away from fixed-size structures. The corresponding structures are bounded, but the bounds are changeable. Although extend requires not full, another feature, called force in all applicable classes, will add an element at the appropriate position by resizing and reallocating the structure if necessary. Even arrays in Eiffel are not fixed-size, and have a procedure force with no precondition, accepting any index position.

The Assertion Redeclaration rule, together with the Invariant Accumulation rule, provides the right methodological perspective for understanding inheritance and the associated mechanisms. Defining a class as inheriting from another is a strong commitment; it means inheriting not only the features but the logical constraints. Redeclaring a routine is bound by a similar committment: to provide a new implementation (or, for an effecting, a first implementation) of a previously defined semantics, as expressed by the original contract. Usually you have a wide margin for choosing your implementation, since the contract only defines a range of possible behaviors (rather than just one behavior), but you must remain within that range. Otherwise you would be perverting the goals of redeclaration, using this mechanism as a sort of late-stage hacking to override bugs in ancestor classes.

Join and uneffecting

It is not an error to inherit two deferred features from different parents under the same name, provided they have the same signature (number and types of arguments and result). In that case a process of feature join takes place: the features are merged into just one -- with their preconditions and postconditions, if any, respectively or-ed and and-ed.

More generally, it is permitted to have any number of deferred features and at most one effective feature that share the same name: the effective version, if present will effect all the others.

All this is not a violation of the Final Name rule (defined in "Multiple inheritance and renaming" ), since the name clashes prohibited by the rule involve two different features having the same final name; here the result is just one feature, resulting from the join of all the inherited versions.

Sometimes we may want to join effective features inherited from different parents, assuming again the features have compatible signatures. One way is to redefine them all into a new version. That is, list each in a redefine clause, then write a redefined version of the feature. In this case, they again become one feature, with no name clash in the sense of the Final Name rule. But in other cases we may simply want one of the inherited implementations to take over the others. The solution is to revert to the preceding case by uneffecting the other features; uneffecting an inherited effective feature makes it deferred (this is the reverse of effecting, which turns an inherited deferred feature into an effective one). The syntax uses the undefine subclause: class D inherit A rename g as f -- g was effective in A undefine f end B undefine f -- f was effective in B end C -- C also has an effective feature f , which will serve as -- implementation for the result of the join. feature ...

Again what counts, to determine if there is an invalid name clash, is the final name of the features. In this example, two of the joined features were originally called f; the one from A was called g, but in D it is renamed as f, so without the undefinition it would cause an invalid name clash.

Feature joining is the most common application of uneffecting. In some non-joining cases, however, it may be useful to forget the original implementation of a feature and let it start a new life devoid of any burden from the past.

Changing the export status

Another Feature Adaptation subclause, export, makes it possible to change the export status of an inherited feature. By default -- covering the behavior desired in the vast majority of practical cases -- an inherited feature keeps its original export status (exported, secret, selectively exported). In some cases, however, this is not appropriate:

A feature may have played a purely implementation-oriented role in the parent, but become interesting to clients of the heir. Its status will change from secret to exported.

In implementation inheritance (for example ARRAYED_LIST inheriting from ARRAY) an exported feature of the parent may not be suitable for direct use by clients of the heir. The change of status in this case is from exported to secret.

You can achieve either of these goals by writingclass D inherit A export {X, Y, ...} feature1, feature2, ... end ...

This gives a new export status to the features listed (under their final names since, as noted, export like all other subclauses comes after rename if present): they become exported to the classes listed. In most cases this list of classes, X, Y, ..., consists of just ANY, to re-export a previously secret feature, or NONE, to hide a previously exported feature. It is also possible, in lieu of the feature list, to use the keyword all to apply the new status to all features inherited from the listed parent. Then there can be more than one class-feature list, as in class ARRAYED_LIST [G] inherit ARRAY [G] rename count as capacity, item as array_item, put as array_put export {NONE} all {ANY} capacity end ...

where any explicit listing of a feature, such as capacity, takes precedence over the export status specified for all. Here most features of ARRAY are secret in ARRAYED_LIST, because the clients should not permitted to manipulate array entries directly: they will manipulate them indirectly through list features such as extend and item, whose implementation relies on array_item and array_put. But ARRAY's feature count remains useful, under the name capacity, to the clients of ARRAYED_LIST.

Flat and Flat-Contract Forms

Thanks to inheritance, a concise class text may achieve a lot, relying on all the features inherited from direct and indirect ancestors.

This is part of the power of the object-oriented form of reuse, but can create a comprehension and documentation problem when the inheritance structures become deep: how does one understand such a class, either as client author or as maintainer? For clients, the Contract Form, entirely deduced from the class text, does not tell the full story about available features; and maintainers must look to proper ancestors for much of the relevant information.

These observations suggest ways to produce, from a class text, a version that is equivalent feature-wise and assertion-wise, but has no inheritance dependency. This is called the Flat Form of the class. It is a class text that has no inheritance clause and includes all the features of the class, immediate (declared in the class itself) as well as inherited. For the inherited features, the flat form must of course take account of all the feature adaptation mechanisms: renaming (each feature must appear under its final name), redefinition, effecting, uneffecting and export status change. For redeclared features, require else clauses are or-ed with the precursors' preconditions, and ensure then clauses are and-ed with precursors' postconditions. For invariants, all the ancestors' clauses are concatenated. As a result, the flat form yields a view of the class, its features and its assertions that conforms exactly to the view offered to clients and (except for polymorphic uses) heirs.

As with the Contract Form ( "The contract form of a class" ), producing the Flat Form is the responsibility of tools in the development environment. In EiffelStudio, you will just click the "Flat" icon.

The Contract Form of the Flat Form of a class is known as its Flat-Contract Form. It gives the complete interface specification, documenting all exported features and assertions -- immediate or inherited -- and hiding implementation aspects. It is the appropriate documentation for a class.

Repeated inheritance and selection

An inheritance mechanism, following from multiple inheritance, remains to be seen. Through multiple inheritance, a class can be a proper descendant of another through more than one path. This is called repeated inheritance and can be indirect, as in the following figure, or even direct, when a class D lists a class A twice in its inherit clause.

The figure's particular example is in fact often used by introductory presentations of multiple inheritance, which is a pedagogical mistake: simple multiple inheritance examples (such as INTEGER inheriting from NUMERIC and COMPARABLE, or COMPANY_PLANE from ASSET and PLANE) should involve the combination of separate abstractions. Repeated inheritance is an advanced technique; although invaluable, it does not arise in elementary uses and requires a little more care.

In fact there is only one non-trivial issue in repeated inheritance: what does a feature of the repeated ancestor, such as change_address and computer_account, mean for the repeated descendant, here TEACHING_ASSISTANT? (The example features chosen involve a routine and an attribute; the basic rules will be the same.)

There are two possibilities: sharing (the repeatedly inherited feature yields just one feature in the repeated descendant) and duplication (it yields two). Examination of various cases shows quickly that a fixed policy, or one that would apply to all the features of a class, would be inappropriate.

Feature change_address calls for sharing: as a teaching assistant, you may be both teacher and student, but you are just one person, with just one official domicile.

If there are separate accounts for students' course work and for faculty, you may need one of each kind, suggesting that computer_account calls for duplication.

The Eiffel rule enables, once again, the software developer to craft the resulting class so as to tune it to the exact requirements. Not surprisingly, it is based on names, in accordance with the Final Name rule (no in-class overloading):

Rule -- Repeated Inheritance:
A feature inherited multiply under one name will be shared: it is considered to be just one feature in the repeated descendant.
A feature inherited multiply under different names will be replicated, yielding as many variants as names.

So to tune the repeated descendant, feature by feature, for sharing and replication it suffices to use renaming.

Doing nothing will cause sharing, which is indeed the desired policy in most cases (especially those cases of unintended repeated inheritance: making D inherit from A even though it also inherits from B, which you forgot is already a descendant of A).

If you use renaming somewhere along the way, so that the final names are different, you will obtain two separate features. It does not matter where the renaming occurs; all that counts is whether in the common descendant, TEACHING_ASSISTANT in the last figure, the names are the same or different. So you can use renaming at that last stage to cause replication; but if the features have been renamed higher you can also use last-minute renaming to avoid replication, by bringing them back to a single name.

The Repeated Inheritance rule gives the desired flexibility to disambiguate the meaning of repeatedly inherited features. There remains a problem in case of redeclaration and polymorphism. Assume that somewhere along the inheritance paths one or both of two replicated versions of a feature f, such as computer_account in the example, has been redeclared; we need to define the effect of a call a.f ( a.computer_account in the example) if a is of the repeated ancestor type, here UNIVERSITY_PERSON, and has become attached as a result of polymorphism to an instance of the repeated descendant, here TEACHING_ASSISTANT. If one or more of the intermediate ancestors has redefined its version of the feature, the dynamically-bound call has two or moreversions to choose from.

A select clause will resolve the ambiguity, as in class TEACHING_ASSISTANT inherit TEACHER rename computer_account as faculty_account select faculty_account end STUDENT rename computer_account as student_account end ...

We assume here that that no other renaming has occurred -- TEACHING_ASSISTANT takes care of the renaming to ensure replication -- but that one of the two parents has redefined computer_account, for example TEACHER to express the special privileges of faculty accounts. In such a case the rule is that one (and exactly one) of the two parent clauses in TEACHING_ASSISTANT must select the corresponding version. Note that no problem arises for an entity declared as ta: TEACHING_ASSISTANT

since the valid calls are of the form ta.faculty_account and ta.student_account, neither of them ambiguous; the call ta.computer_account would be invalid, since after the renamings class TEACHING_ASSISTANT has no feature of that name. The select only applies to a call up.computer_accountwith up of type UNIVERSITY_PERSON, dynamically attached to an instance of TEACHING_ASSISTANT; then the select resolves the ambiguity by causing the call to use the version from TEACHER.

So if you traverse a list computer_users: LIST [UNIVERSITY_PERSON] to print some information about the computer account of each list element, the account used for a teaching assistant is the faculty account, not the student account.

You may, if desired, redefine faculty_account in class TEACHING_ASSISTANT, using student_account if necessary, to take into consideration the existence of another account. But in all cases we need a precise disambiguation of what computer_account means for a TEACHING_ASSISTANT object known only through a UNIVERSITY_PERSON entity.

The select is only needed in case of replication. If the Repeated Inheritance rule would imply sharing, as with change_address, and one or both of the shared versions has been redeclared, the Final Name rule makes the class invalid, since it now has two different features with the same name. (This is only a problem if both versions are effective; if one or both are deferred there is no conflict but a mere case of feature joining as explained in "Join and uneffecting" .) The two possible solutions follow from the previous discussions:

If you do want sharing, one of the two versions must take precedence over the other. It suffices to undefine the other, and everything gets back to order. Alternatively, you can redefine both into a new version, which takes precedence over both.

If you want to keep both versions, switch from sharing to replication: rename one or both of the features so that they will have different names; then you must select one of them.

Constrained genericity

Eiffel's inheritance mechanism has an important application to extending the flexibility of the genericity mechanism. In a class SOME_CONTAINER [G], as noted in "Genericity and Arrays" ), the only operations available on entities of type G, the formal generic parameter, are those applicable to entities of all types. A generic class may, however, need to assume more about the generic parameter, as with a class SORTABLE_ARRAY [G ...] which will have a procedure sort that needs, at some stage, to perform tests of the form if item (i) < item (j) then ...where item (i) and item (j) are of type G. But this requires the availability of a feature infix "<" in all types that may serve as actual generic parameters corresponding to G. Using the type SORTABLE_ARRAY [INTEGER] should be permitted, because INTEGER has such a feature; but not SORTABLE_ARRAY [COMPLEX] if there is no total order relation on COMPLEX.

To cover such cases, declare the class asclass SORTABLE_ARRAY [G -> COMPARABLE]making it constrained generic. The symbol -> recalls the arrow of inheritance diagrams; what follows it is a type, known as the generic constraint. Such a declaration means that:

Within the class, you may apply the features of the generic constraint -- here the features of COMPARABLE: infix "<", infix ">" etc. -- to expressions of type G.

A generic derivation is only valid if the chosen actual generic parameter conforms to the constraint. Here you can use SORTABLE_ARRAY [INTEGER] since INTEGER inherits from COMPARABLE, but not SORTABLE_ARRAY [COMPLEX] if COMPLEX is not a descendant of COMPARABLE.

A class can have a mix of constrained and unconstrained generic parameters, as in the EiffelBase class HASH_TABLE [G, H -> HASHABLE] whose first parameter represents the types of objects stored in a hash table, the second representing the types of the keys used to store them, which must be HASHABLE. As these examples suggest, structural property classes such as COMPARABLE, NUMERIC and HASHABLE are the most common choice for generic constraints.

Unconstrained genericity, as in C [G], is defined as equivalent to C [G -> ANY].

Assignment attempt

Caution: As of version 7.1, the assignment attempt has been marked as obsolete. Use the object test (described below in a variant of this same discussion) instead. The documentation for the assignment attempt will remain during a period of transition, but will be removed at some point in the future.

The Type Conformance rule ( "Polymorphism" ) ensures type safety by requiring all assignments to be from a more specific source to a more general target.

Sometimes you can't be sure of the source object's type. This happens for example when the object comes from the outside -- a file, a database, a network. The persistence storage mechanism( "Deep operations and persistence" ) includes, along with the procedure store seen there, the reverse operation, a function retrieved which yields an object structure retrieved from a file or network, to which it was sent using store. But retrieved as declared in the corresponding class STORABLE of EiffelBase can only return the most general type, ANY; it is not possible to know its exact type until execution time, since the corresponding objects are not under the control of the retrieving system, and might even have been corrupted by some external agent.

In such cases you cannot trust the declared type but must check it against the type of an actual run-time object. Eiffel introduces for this purpose the assignment attempt operation, written x ?= y

with the following effect (only applicable if x is a writable entity of reference type):

If y is attached, at the time of the instruction's execution to an object whose type conforms to the type of x, perform a normal reference assignment.

Otherwise (if y is void, or attached to a non-conforming object), make x void.

Using this mechanism, a typical object structure retrieval will be of the form x ?= retrieved if x = Void then "We did not get what we expected" else "Proceed with normal computation, which will typically involve calls of the form x.some_feature " end

As another application, assume we have a LIST [ACCOUNT] and class SAVINGS_ACCOUNT, a descendant of ACCOUNT, has a feature interest_rate which was not in ACCOUNT. We want to find the maximum interest rate for savings accounts in the list. Assignment attempt easily solves the problem: local s: SAVINGS_ACCOUNT do from account_list.start until account_list.after loop s ?= acc_list.item -- item from LIST yields the element at -- cursor position if s /= Void and then s.interest_rate > Result then -- Using and then (rather than and) guarantees -- that s.interest_rate is not evaluated -- if s = Void is true. Result := s.interest_rate end account_list.forth end end

Note that if there is no savings account at all in the list the assignment attempt will always yield void, so that the result of the function will be 0, the default initialization.

Assignment attempt is useful in the cases cited -- access to external objects beyond the software's own control, and access to specific properties in a polymorphic data structure. The form of the instruction precisely serves these purposes; not being a general type comparison, but only a verification of a specific expected type, it does not carry the risk of encouraging developers to revert to multi-branch instruction structures, for which Eiffel provides the far preferable alternative of polymorphic, dynamically-bound feature calls.

Object test

The Type Conformance rule ( "Polymorphism" ) ensures type safety by requiring all assignments to be from a more specific source to a more general target.

Sometimes you can't be sure of the source object's type. This happens for example when the object comes from the outside -- a file, a database, a network. The persistence storage mechanism( "Deep operations and persistence" ) includes, along with the procedure store seen there, the reverse operation, a function retrieved which yields an object structure retrieved from a file or network, to which it was sent using store. But retrieved as declared in the corresponding class STORABLE of EiffelBase can only return the most general type, ANY; it is not possible to know its exact type until execution time, since the corresponding objects are not under the control of the retrieving system, and might even have been corrupted by some external agent.

In such cases you cannot trust the declared type but must check it against the type of an actual run-time object. Eiffel introduces for this purpose the object test operation, using a form of the attached syntax. The complete attached syntax is: attached {SOME_TYPE} exp as l_exp

and is a boolean-valued expression. So we can use the attached syntax as an object test. A typical object structure retrieval will be of the form if attached retrieved as l_temp then -- We got what we expected -- Proceed with normal computation, typically involving calls of the form l_temp.some_feature else -- We did not get what we expected" end

The expression attached retrieved as l_temp tests retrieved for voidness. If retrieved is not void, that is, retrieved is currently attached to an object, then a fresh local entity l_temp is created, the object is attached to l_temp, and the value of the expression is True. If retrieved is void, then the value of the expression is False.

As another application, assume we have a LIST [ACCOUNT] and class SAVINGS_ACCOUNT, a descendant of ACCOUNT, has a feature interest_rate which was not in ACCOUNT. We want to find the maximum interest rate for savings accounts in the list. Object test easily solves the problem: do from account_list.start until account_list.after loop if attached {SAVINGS_ACCOUNT} account_list.item as l_s and then l_s.interest_rate > Result then -- Using and then (rather than and) guarantees -- that l_s.interest_rate is not evaluated -- if `attached {SAVINGS_ACCOUNT} account_list.item as l_s' is False. Result := l_s.interest_rate end account_list.forth end end

Note that if there is no savings account at all in the list the object test will never be satisfied, so that the result of the function will be 0, the default initialization.

The object test is useful also in building void-safe software systems.

See Also: More about the attached syntax in the section on Void-safe programming in Eiffel.

Covariance, anchored declarations, and "catcalls"

The final properties of Eiffel inheritance involve the rules for adapting not only the implementation of inherited features (through redeclaration of either kind, effecting and redefinition, as seen so far) and their contracts (through the Assertion Redeclaration rule), but also their types. More general than type is the notion of a feature's signature, defined by the number of its arguments, their types, the indication of whether it has a result (that is to say, is a function or attribute rather than a procedure) and, if so, the type of the result.

Covariance

In many cases the signature of a redeclared feature remains the same as the original's. But in some cases you may want to adapt it to the new class. Assume for example that class ACCOUNT has features owner: HOLDER set_owner (h: HOLDER) -- Make `h' the account owner. require not_void: h /= Void do owner := h end

We introduce an heir BUSINESS_ACCOUNT of ACCOUNT to represent special business accounts, corresponding to class BUSINESS inheriting from HOLDER:

Clearly, we must redefine owner in class BUSINESS_ACCOUNT to yield a result of type BUSINESS; the same signature redefinition must be applied to the argument of set_owner. This case is typical of the general scheme of signature redefinition: in a descendant, you may need to redefine both results and arguments to types conforming to the originals. This is reflected by a language rule:

Rule -- Covariance: In a feature redeclaration, both the result type if the feature is a query (attribute or function) and the type of any argument if it is a routine (procedure or function) must conform to the original type as declared in the precursor version.

The term "covariance" reflects the property that all types -- those of arguments and those of results -- vary together in the same direction as the inheritance structure.

If a feature such as set_owner has to be redefined for more than its signature -- to update its implementation or assertions -- the signature redefinition will be explicit. For example set_owner could do more for business owners than it does for ordinary owners. Then the redefinition will be of the form set_owner (b: BUSINESS) -- Make b the account owner. do ... New routine body ... end

Anchored Declarations

In other cases, however, the body will be exactly the same as in the precursor. Then explicit redefinition would be tedious, implying much text duplication. The mechanism of anchored redeclaration solves this problem. The original declaration of set_owner in ACCOUNT should be of the form set_owner (h: like owner) -- Make h the account owner. -- The rest as before: require not_void: h /= Void do owner := h end

A like anchor type, known as an anchored type, may appear in any context in which anchor has a well-defined type; that is, anchor can be an attribute or function of the enclosing class. Then, assuming T is the type of anchor, the type like anchor means the following:

In the class in which it appears, like anchor means the same as T. For example, in set_owner above, the declaration of h has the same effect as if h had been declared of type HOLDER, the type of the anchor owner in class ACCOUNT.

The difference comes in proper descendants: if a type redefinition changes the type of anchor, any entity declared like anchor will be considered to have been redefined too.

This means that anchored declarations are a form of of implicit covariant redeclaration.

In the example, class BUSINESS_ACCOUNT only needs to redefine the type of owner (to BUSINESS). It doesn't have to redefine set_owner except if it needs to change its implementation or assertions.

It is possible to use Current as anchor; the declaration like Current denotes a type based on the current class (with the same generic parameters if any). This is in fact a common case; we saw in "Structural property classes" , that it applies in class COMPARABLE to features such as is_less alias "<" (other: like Current): BOOLEAN ...

since we only want to compare two comparable elements of compatible types -- but not, for example, integer and strings, even if both types conform to COMPARABLE. (A "balancing rule" makes it possible, however, to mix the various arithmetic types, consistently with mathematical traditions, in arithmetic expressions such as 3 + 45.82 or boolean expressions such as 3 < 45.82.)

Similarly, class ANY declares procedure copy as copy (other: like Current) ...

with the argument anchored to the current object.

A final, more application-oriented example of anchoring to Current is the feature merge posited in an earlier example (in "The Dynamic Structure: Execution Model" ) with the signature merge (other: ACCOUNT). By using instead merge (other: like Current) we can ensure that in any descendant class -- BUSINESS_ACCOUNT, SAVINGS_ACCOUNT, MINOR_ACCOUNT ... -- an account will only be mergeable with another of a compatible type.

Qualified Anchored Declarations

The anchored types shown above specify anchors which are either:

  • The name of a query of the class in which the anchored declaration appears
    • as in the case of: set_owner (h: like owner) or
  • Current
    • as in the case of: is_less alias "<" (other: like Current): BOOLEAN.

Declarations can also use qualified anchored types. Consider this possible feature of ACCOUNT:

owner_name: like owner.name

Here the type of owner_name is determined as the type of the feature name as applied to the type of the feature owner of the current class. As you can imagine, for declarations like this to be valid, the feature names name and owner must be the names queries, i. e., the names of attributes or functions.

This notion can be extended to declare the type through multiple levels of remoteness, so patterns like the following can be valid:

f: like a.b.c.d

For example if a class used a list of items of type ACCOUNT, it might include a declaration for that list:

all_accounts: LINKED_LIST [ACCOUNT] -- All my accounts

This class could declare a feature with a qualified anchored type like this:

account_owner_name: like all_accounts.item.owner.name

A qualified anchored type can be qualified also by specifying a type for the qualifier:

owner_name: like {HOLDER}.name

In this case, the type of owner_name is the same as the type of the name feature of type HOLDER.

Anchored declarations serve as another way to make software more concise and more resilient in a changing world. Let's look at one last example of using a qualified anchored type:

a: ARRAY [DATA] ... local idx: like a.lower do from idx := a.lower until idx > a.upper ...

Declaring the local entity idx as the qualified anchored type like a.lower puts this code (well, actually the producer of this code) in the enviable position of never having to worry about what type is used by class ARRAY for its feature lower. So, {ARRAY}.lower could be implemented as INTEGER_32, NATURAL_64, or some other similar type and this code would be fine, even if at some point that type changed.

Catcalls

In our diversion about anchored declarations, we've gotten away from our discussion of covariance. Let's continue that now with a look at a side effect of covariance known as the catcall.

Covariance makes static type checking more delicate; mechanisms of system validity and catcalls address the problem, discussed in detail in the book Object-Oriented Software Construction, 2nd Edition.

The capabilities of polymorphism combined with covariance provide for powerful and flexible modeling. Under certain conditions, though, this flexibility can lead to problems.

In short, you should be careful to avoid polymorphic catcalls. The call part of catcall means feature call. The cat part is an acronym for Changed Availability or Type. What is changing here are features of descendant classes through the adaptation of inheritance. So maybe a descendant class has changed the export status of an inherited feature, so that that feature is not available on instances of the descendant class ... this is the case of changed availability. Or perhaps, through covariant modeling, the type of an argument to a feature in a descendant class has changed ... the case of changed type.

Let's look at an example of changed type, due to covariant modeling. Suppose we have a system which uses the classes depicted on the following diagram:

If in a client class, we declare the following attributes:

my_animal: ANIMAL my_food_stuff: FOOD_STUFF

Also, the class ANIMAL contains the feature: eat (a_f: FOOD_STUFF) -- Consume `a_f' deferred endThis routine is implemented in COW as: eat (a_f: GRASS)and in class LION as: eat (a_f: WILDEBEEST_FILET)

So, covariant modeling is used to make the type of the argument for eat appropriate for each of ANIMAL's heirs.

Here's where the problem comes in. It is possible at run-time to attach to my_animal a direct instance of either COW or LION. So, my_animal is a polymorphic attribute. Likewise, it is possible at run-time that my_food_stuff could be attached to a direct instance of either GRASS or WILDEBEEST_FILET.

So, the feature call: my_animal.eat (my_food_stuff) is a catcall, because there is a possibility that through the changing type of the argument to eat, we could be causing a COW to engage in the inappropriate practice of eating a WILDEBEEST_FILET.

Because this possibility exists, developers should exercise caution in using polymorphism and covariant modeling.

In version 6.2 of EiffelStudio, a capability was added to detect harmful catcalls at runtime. So, in our example, if we used my_animal.eat (my_food_stuff) only to feed grass to cows and wildebeest filets to lions, then all would be well. But if we attempted to use that same call to feed an inappropriate food to an animal, we would see an exception.

Likewise the compiler in EiffelStudio will produce warnings in cases in which catcalls are possible. Below is an example of the compiler warning issued on the example.

Non-conforming inheritance

So far, our experience with inheritance is that of "conforming" inheritance ... the most commonly used type of inheritance. Conforming inheritance is what allows a direct instance (in the catcall example above) of COW to be attached at runtime to an entity of type ANIMAL. This can be a powerful modeling capability, but it is this same polymorphism facilitated by conforming inheritance that puts us in the danger of using polymorphic catcalls.

In cases in which polymorphic attachment is not anticipated, the possibility of catcalls can be avoided by using non-conforming inheritance. Non-conforming inheritance is just a more restrictive form of inheritance.

Non-conforming inheritance allows features to be inherited from parent to heir, but it disallows polymorphic attachment of a direct instance of an heir to an entity based on a non-conforming parent.

In order to use non-conforming inheritance for a particular parent, we use the marker {NONE} in the appropriate inheritance part of the class:

class MY_HEIR_CLASS inherit MY_CONFORMING_PARENT inherit {NONE} MY_NON_CONFORMING_PARENT ...

Here there are two inherit clauses, one to specify conforming parents, and one to specify non-conforming parents. The clause specifying the conforming inheritance must precede the one specifying the non-conforming inheritance.

Note: According to the Eiffel programming language standard, it is possible to have any number of inherit clauses an any order, however EiffelStudio versions as late as 6.5 allow only one conforming and one non-conforming clause, with the conforming clause preceding the non-conforming one. This restriction will be removed in a future release.

So, in this case, at runtime it is valid for a direct instance of MY_HEIR_CLASS to be attached to an entity of type MY_CONFORMING_PARENT, but not to an entity of type MY_NON_CONFORMING_PARENT. Accordingly, the compiler would reject any code in which an instance of MY_HEIR_CLASS could become attached to an entity of type MY_NON_CONFORMING_PARENT. Because the polymorphic attachment cannot be made, the possibility of a catcall is avoided.

Note: As implemented, non-conforming inheritance mimics a copy/paste operation in which the features of the parent class are copied to the non-conforming heir class with no inheritance linkage maintained. You should keep this fact in mind when using non-conforming inheritance. In particular, once routines are replicated as unrelated features in the heir classes, so they share neither freshness status nor computed value (in the case of functions). Thus, a once function that comes from a non-conforming parent yields a result that is not related to the one returned by the parent's version.

cached: 03/18/2024 8:27:30.000 PM