- Tags:
- inheritance
ET: Inheritance
- Contents
- Basic inheritance structure
- Redefinition
- Polymorphism
- Dynamic binding
- Deferred features and classes
- Applications of deferred classes
- Structural property classes
- Multiple inheritance and renaming
- Inheritance and contracts
- Join and uneffecting
- Changing the export status
- Flat and Flat-Contract Forms
- Repeated inheritance and selection
- Constrained genericity
- Assignment attempt
- Object test
- Covariance, anchored declarations, and "catcalls"
- Non-conforming 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.
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
.
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:
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.
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
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:
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:
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 p
For 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:
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):
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_account
with 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.
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:
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
- as in the case of:
-
Current
- as in the case of:
is_less alias "<" (other: like Current): BOOLEAN
.
- as in the case of:
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
end
This 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.
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.