Thread View: comp.lang.eiffel
2 messages
2 total messages
Started by dcr0@bunny.UUCP
Mon, 09 Jan 1989 20:06
Conflict Between Class-as-Module and Class-as-Type (long)
Author: dcr0@bunny.UUCP
Date: Mon, 09 Jan 1989 20:06
Date: Mon, 09 Jan 1989 20:06
152 lines
7615 bytes
7615 bytes
(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
Re: Conflict Between Class-as-Module and Class-as-Type (long)
Author: jos@cs.vu.nl (Jo
Date: Tue, 10 Jan 1989 08:53
Date: Tue, 10 Jan 1989 08:53
112 lines
4229 bytes
4229 bytes
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
Thread Navigation
This is a paginated view of messages in the thread with full content displayed inline.
Messages are displayed in chronological order, with the original post highlighted in green.
Use pagination controls to navigate through all messages in large threads.
Back to All Threads