[comp.lang.eiffel] Conflict Between Class-as-Module and Class-as-Type

dcr0@bunny.UUCP (David Robbins) (01/10/89)

(My apologies for the length of this article.  There is a fairly significant
point to be discussed here, and I've tried to be as concise as possible.)

While writing up the posting about an apparent anomaly in Eiffel's
inheritance mechanism, a closely-related possibility came to mind.  I've
tried it, and sure enough, Eiffel implements inheritance in a way which
I'm fully convinced is inappropriate.

I have three classes named ROOT_CLASS, A, and B, defined as follows:

root_class.e --

   class ROOT_CLASS
   inherit STD_FILES
   feature
      an_a: A; a_b: B; b_invoked_as_a: A;
      Create is do
	 an_a.Create; a_b.Create; b_invoked_as_a := a_b;
	 putstring("Calling A.f1:  "); an_a.f1;
	 putstring("Calling A.f2:  "); an_a.f2;
	 putstring("Calling B.f2:  "); a_b.f2;
	 putstring("Calling B.f3 (which is really A.f1): "); a_b.f3;
	 putstring("Calling B.f1 through A's interface:  "); b_invoked_as_a.f1;
	 putstring("Calling B.f2 through A's interface:  "); b_invoked_as_a.f2
      end
   end

a.e --

   class A export f1, f2
   inherit STD_FILES
   feature
      f1 is do putstring("I am A.f1"); new_line end;
      f2 is do putstring("I am A.f2"); new_line end
   end

b.e --

   class B export f2, f3
   inherit A rename f1 as f3 redefine f2
   feature
      f2 is do putstring("I am B.f2"); new_line end
   end

Results of Execution --

   Calling A.f1:  I am A.f1
   Calling A.f2:  I am A.f2
   Calling B.f2:  I am B.f2
   Calling B.f3 (which is really A.f1): I am A.f1
   Calling B.f1 through A's interface:  I am A.f1
   Calling B.f2 through A's interface:  I am B.f2

This is almost identical to my previous example, but here B does not
export f1.  Since B does not export f1, I would certainly not expect
"b_invoked_as_a.f1" to succeed.  After all, it is trying to call feature f1
of an instance of B, which does not export any such feature.  In fact, B
does not even possess a feature named f1!

This behavior violates my expectation of what I tend to call "specification
inheritance."  In a typed object-oriented language, it seems axiomatic to
me that a class should be required to implement the same behavior as its
ancestor(s), at least to the extent of exporting at least the same set
of features, each being able to be used in the same way as the corresponding
features of the ancestor(s).  In other words, if classes are truly to be
thought of as types, it is required that a class export every feature
that its ancestor(s) export, and that each feature redefined in the
class have an interface that is compatible, in some useful sense, with
the corresponding ancestor feature.

The definition of B above also is contrary to the claim that selective
inheritance is not supported by Eiffel.  In chapter 10.5.3 of The Book, Meyer
discusses selective inheritance, concluding that Eiffel should not allow
a class to reject part of its heritage.  In one sense, my class B has
not rejected its heritage: it inherits f1 from A, but changes its name.
In another sense, however, B has indeed rejected its heritage, for it
no longer possesses a feature named f1.

Eiffel, however, explicitly does NOT require a class to export every
feature exported by its ancestor(s).  In The Book, chapter 11.5
discusses various reasons for differences between the exports of a
class and of its descendants.  The discussion begins by stating that
a class and its ancestor can independently decide whether or not to
export a given feature.  The motivation for this is information hiding.
Meyer discusses the case of a class exporting a feature of its ancestor
which the ancestor does not export.  He does not, however, consider here the
case of the ancestor exporting a feature that its descendant does not export.
But in chapter 14.4.5, the use of inheritance to gain access to
general-purpose facilities (e.g., STD_FILES) is encouraged.  This use of
inheritance is critically dependent upon the ability of the class to refuse
to export features that were exported by an ancestor.

Eiffel does have the intention of treating classes as types, and therefore
descendant classes as subtypes (see 10.2.2 of The Book).  The rules for
compatibility of a redefined feature with that of its ancestor clearly bear
this out.  Meyer states, in 10.1.4 of The Book: "Once a system has been
compiled, there is no risk that a feature will ever be applied at run-time
to an object that is not equipped to handle it."  My example above is a
counterexample to this claim: class B is totally unequipped to handle
feature f1 (although the Eiffel implementation manages to secretly give B a
feature named f1, in violation of the semantics of Eiffel).

It is my conclusion that there is an unfortunate interaction here between
(1) the desire to treat classes as types and (2) the desire to support
information hiding and general-purpose facilities (as in 14.4.5) by allowing
the descendant to refuse to export -- or even to possess -- a feature
exported by its ancestor.  In order to properly treat types as classes,
it is necessary that Eiffel require a class to export every feature that
is exported by its ancestors.  Without such a requirement, absurdities
such as my example above may occur, with no warning whatsoever to the
unfortunate programmer who creates such a situation by accident.

If such a requirement is made, the notions of information hiding as
exemplified in 11.5 of The Book can still be supported.  The ideas there
are for a class to extend the interface of its ancestor by exporting
some features that the ancestor did not export.  This is completely
consistent with the view of classes as types, where a subtype is
permitted to extend the interface of its supertype.

The dark side of all this is that the requirement I propose would render
illegal all the Eiffel programs that use inheritance as a means to gain
access to utility classes like STD_FILES, as encouraged in 14.4.5 of The
Book.  When I first saw how inheritance was being used for this purpose,
I had this eerie feeling that something was seriously wrong with using
inheritance this way.  Now I know what that eerie feeling meant.  This
particular use of inheritance is fundamentally and inherently in conflict
with the idea that a class is a type.  As Meyer points out, STD_FILES can
be used as a type, as illustrated in 5.6.4 of The Book.  But he encourages
the use of inheritance to, in effect, treat STD_FILES not as a type but
as a module after the fashion of an Ada package or a Modula-2 module.  A
class rarely inherits from STD_FILES for the purpose of becoming a subtype
of STD_FILES; it usually just wants to use the features of STD_FILES.

And here, I regretfully reach the conclusion that Eiffel has painted itself
into a corner, as it were.  Given the express desire to treat classes as
types, and the standardization of the practice of treating classes as
modules in a way totally in conflict with treatment as types, Eiffel is in
a position from which it can be extricated only with considerable effort.
My recommendation would be to introduce a language feature by which a class
meant to be a type is explicitly distinguished from a class meant to be used
only as a module.  Only by so doing can the concept of class as type be
properly and fully supported while continuing to support the use of classes
as modules but not types.

But perhaps I have missed a significant point that proves there is no
conflict.  Any comments from the faithful comp.lang.eiffel readers?
-- 

Dave Robbins                    GTE Laboratories Incorporated
drobbins@gte.com                40 Sylvan Rd.
...!harvard!bunny!drobbins      Waltham, MA 02254

jos@cs.vu.nl (Jos Warmer) (01/10/89)

In article <6417@bunny.UUCP> dcr0@bunny.UUCP (David Robbins) writes:
>
>I have three classes named ROOT_CLASS, A, and B, defined as follows:
>
>   class ROOT_CLASS
>   inherit STD_FILES
>   feature
>      an_a: A; a_b: B; b_invoked_as_a: A;
>      Create is do
>	 an_a.Create; a_b.Create; b_invoked_as_a := a_b;
>	 putstring("Calling A.f1:  "); an_a.f1;
>	 putstring("Calling A.f2:  "); an_a.f2;
>	 putstring("Calling B.f2:  "); a_b.f2;
>	 putstring("Calling B.f3 (which is really A.f1): "); a_b.f3;
>	 putstring("Calling B.f1 through A's interface:  "); b_invoked_as_a.f1;
>	 putstring("Calling B.f2 through A's interface:  "); b_invoked_as_a.f2
>      end
>   end
>
>   class A export f1, f2
>   inherit STD_FILES
>   feature
>      f1 is do putstring("I am A.f1"); new_line end;
>      f2 is do putstring("I am A.f2"); new_line end
>   end
>
>   class B export f2, f3
>   inherit A rename f1 as f3 redefine f2
>   feature
>      f2 is do putstring("I am B.f2"); new_line end
>   end
>
>Results of Execution --
>
>   Calling A.f1:  I am A.f1
>   Calling A.f2:  I am A.f2
>   Calling B.f2:  I am B.f2
>   Calling B.f3 (which is really A.f1): I am A.f1
>   Calling B.f1 through A's interface:  I am A.f1
>   Calling B.f2 through A's interface:  I am B.f2
>
>export f1.  Since B does not export f1, I would certainly not expect
>"b_invoked_as_a.f1" to succeed.  After all, it is trying to call feature f1
>of an instance of B, which does not export any such feature.  In fact, B
>does not even possess a feature named f1!
>

This behaviour is correctly defined in eiffel. It is even neccesary to define
this behaviour if you want compile-time checking.
The entity "b_invoked_as_a" has static type A.  It is impossible for the
compiler to know the dynamic type of "b_invoked_as_a". So if the compiler has
to check whether some feature is applicable to "b_invoked_as_a", it can only
look at the features of A. If a feature is not redefined, then the definition
from A is used. This choice can only be made at runtime.

The rules are:
    The features that may be applied to an entity depend on the static type
    of the entity.  The actual definition used may depend on the dynamic type.

The call "b_invloked_as_a.f3" will not be allowed by the compiler,
because it is not exported by class A.

This behaviour can also be undesirable when using implementation inheritance,
as I stated in a previously posted article.

++ At page 241 of Bertand Meyer's book an alternate definition is
++ given for STACK2 (page 118): the class FIXED_STACK.  It is declared
++ an heir of class ARRAY, instead of a client.
++ 
++ As far as I can see this definition of FIXED_STACK has a serious
++ safety-leak, as opposed to the definition of STACK2 at page 118.
++ 
++ Consider the following piece of (pseudo) eiffel code:
++ 
++     a : ARRAY[INTEGER];         -- declare an entity of type ARRAY
++     f : FIXED_STACK[INTEGER];   -- declare an entity of type FIXED_STACK
++ 
++     f.Create;        -- created a fixed stack
++     f.push(12);      -- top of stack is now 12
++ 
++     a := f;          -- allowed, because FIXED_STACK is descendant of ARRAY
++     a.enter(1, 20);  -- enter is allowed on ARRAY's
++ 
++     value := f.top:  -- this will deliver value 20, instead of the previously
++                      -- entered 12.
++ 
++ This example shows that any client of FIXED_STACK can manipulate its
++ implementation.

>This behavior violates my expectation of what I tend to call "specification
>inheritance." 

Inheritance in eiffel is always inheritance of the complete implementation.

>counterexample to this claim: class B is totally unequipped to handle
>feature f1 (although the Eiffel implementation manages to secretly give B a
>feature named f1, in violation of the semantics of Eiffel).

class B is a descendant of class A, so it IS equipped to handle feature f1.

                                 Jos Warmer
				 jos@cs.vu.nl
				 ...uunet!mcvax!cs.vu.nl!jos

PS. We have ordered the compiler, but it hasn't arrived yet.
    So I can't try anything out.  This is not too bad, now I actually
    have to *think* about it.
-- 
                                 Jos Warmer
				 jos@cs.vu.nl
				 ...uunet!mcvax!cs.vu.nl!jos

dcr0@bunny.UUCP (David Robbins) (01/11/89)

From article <1884@vlot.cs.vu.nl>, by jos@cs.vu.nl (Jos Warmer):
> In article <6417@bunny.UUCP> dcr0@bunny.UUCP (David Robbins [me!]) writes:
>>
>> ...
>>
>>export f1.  Since B does not export f1, I would certainly not expect
>>"b_invoked_as_a.f1" to succeed.  After all, it is trying to call feature f1
>>of an instance of B, which does not export any such feature.  In fact, B
>>does not even possess a feature named f1!
>>
> 
> This behaviour is correctly defined in eiffel. It is even neccesary to define
> this behaviour if you want compile-time checking.
> The entity "b_invoked_as_a" has static type A.  It is impossible for the
> compiler to know the dynamic type of "b_invoked_as_a". So if the compiler has
> to check whether some feature is applicable to "b_invoked_as_a", it can only
> look at the features of A. If a feature is not redefined, then the definition
> from A is used. This choice can only be made at runtime.
> 
> The rules are:
>     The features that may be applied to an entity depend on the static type
>     of the entity.  The actual definition used may depend on the dynamic type.
> 

It is precisely these static checking rules with which Eiffel's support of
renaming combined with selective export conflict!  In the absence of renaming
and selective export, it is guaranteed that any subclass of A will possess
an exported definition of all features exported by A; a feature may be
inherited from A or redefined by B.

Selective export throws a big monkey wrench into this scheme of things.  With
selective export, it becomes possible for B to refuse to export a feature
that A exports.  When B does so, that feature is no longer visible to a
client of B, and Eiffel's static checking of references through static type
B reflects this fact.  But this gives rise to a problem that Eiffel does
not solve: when B is allowed to refuse to export a feature that A exports, it
is not possible to statically check whether a use of that feature is valid.
Since it remains valid to assign an object of class B to a variable with
static type A, the reference to "b_invoked_as_a.f1" cannot be statically
checked!

Renaming also throws a monkey wrench into things.  With renaming, it becomes
possible for B to inherit feature f1 from A, but for f1 to be known within B
as f3.  B is then free to locally define a feature named f1.  But B is also
free to leave the name f1 undefined!  This would not be a problem were it
not for selective export, for without selective export B would at least be
required to have some definition of f1 if A exported an f1.  But in any case,
Eiffel's implementation seems to have a problem with renaming: I posted another
example in which A.f1 was renamed as B.f3 and B.f1 was locally defined; here,
if B is called through A's interface, A's f1 is used, even though B does have
its own definition of f1.  This seems very inconsistent.

Anyway, I will reiterate what I think is the crucial point of my original
posting and another followup I posted:

   Selective export really changes the meaning of inheritance in Eiffel!

It is the export list that defines what features of a class are visible to
clients of the class, and the export list neither gets inherited nor is
subject to any rules about what can and cannot be exported.

Study my two examples and think carefully about the consequences of the
combination of inheritance, renaming, and selective export.-- 

Dave Robbins                    GTE Laboratories Incorporated
drobbins@gte.com                40 Sylvan Rd.
...!harvard!bunny!drobbins      Waltham, MA 02254

dcr0@bunny.UUCP (David Robbins) (01/11/89)

In article <1884@vlot.cs.vu.nl>, jos@cs.vu.nl (Jos Warmer) writes:
> In article <6417@bunny.UUCP> dcr0@bunny.UUCP (David Robbins [me!]) writes:
>
>>This behavior violates my expectation of what I tend to call "specification
>>inheritance." 
>
> Inheritance in eiffel is always inheritance of the complete implementation.
> 
>>counterexample to this claim: class B is totally unequipped to handle
>>feature f1 (although the Eiffel implementation manages to secretly give B a
>>feature named f1, in violation of the semantics of Eiffel).
> 
> class B is a descendant of class A, so it IS equipped to handle feature f1.

That turns out not to be the case.  While it is true that B is a descendant of
A, I cannot interpret the Eiffel language definition to say that B is equipped
to handle feature f1 in my example.  Specifically, in my example, B has indeed
inherited f1 from A, but in B f1 was renamed and is known as f3.  Further, B
has not exported a feature named f1, and hence cannot claim to be able to
handle f1, even if a definition of f1 were to be available in B.  To be
equipped to handle f1, B must first of all export a feature named f1, and then
have an implementation of f1, either by inheriting one or by locally defining
one.

The semantics of renaming can be one of two things: (1) the original name
becomes undefined, making it possible to reuse the original name, or (2) the
original name remains defined, making it impossible to reuse the original name.
Eiffel's intention appears to be the former, as I can modify my example to
have B define its own f1 after renaming the inherited f1.

The ability of B to refuse to export a feature it inherited from A, which
feature WAS exported by A, is what confuses things here.  In all other
object-oriented languages with which I am familiar, a subclass exports
everything that its parent(s) export -- either by virtue of the lack of
explicit control over exports (e.g., Smalltalk-80) or by virtue of rules
that do not permit a subclass to refuse to export what its parent(s) export.
But in Eiffel, a subclass DOES NOT inherit the export list of its parent(s).
I found this out the hard way, when coding an Eiffel program thinking that
I wouldn't have to repeat in the subclass the export list of its parent.

This selective export capability is what really confuses the issue.  The fact
that a feature is defined in a class does not necessarily mean that the feature
can be used by a client: only if a feature is exported by the class can it be
so used.  This is true whether the feature is locally defined or inherited.
Thus, to reiterate my original point: even though class B is a descendant of
class A, it is not equipped to handle feature f1 unless B has explicitly
exported a feature named f1.  It is not sufficient for B to have inherited
a definition for f1 (although in my example B doesn't even have that!).

Study both of the examples I have posted to the net, and think very carefully
about the implications of inheritance combined with renaming combined with
selective export.  It took me several hours of thought to understand Eiffel's
problem with the combination of these three things.  Selective export really
confuses the meaning of inheritance in Eiffel!

-- 

Dave Robbins                    GTE Laboratories Incorporated
drobbins@gte.com                40 Sylvan Rd.
...!harvard!bunny!drobbins      Waltham, MA 02254