chrisv@runx.oz.au (Chris Velevitch) (01/24/91)
I disagree with allowing a class to access inherited features that are not exported. It does not make sense that secret features are known to heir of a class. If a feature is not publicly known, then how can you know about the feature to use in the descendent class. Eiffel allows a class to be designed so it has no interface, which can then be used in descendent classes. What use is a black box in which you cannot put anything in or take anything out. When designing a class, a designer makes the interface public, not its implementation. The user of a class buys services from the class based solely from its publically known behaviour and features. You can only inherit features that are known. I consider inheritance as a form of buying services. -- Chris Velevitch RUNX Unix Timeshare | Internet: chrisv@runxtsa.runx.oz.au Sydney NSW, Australia. | UUCP: uunet!runxtsa.runx.oz.au!chrisv
barmar@think.com (Barry Margolin) (01/25/91)
In article <1991Jan23.224203.3206@runx.oz.au> chrisv@runx.oz.au (Chris Velevitch) writes: >I disagree with allowing a class to access inherited features that are >not exported. It does not make sense that secret features are known to >heir of a class. If a feature is not publicly known, then how can you >know about the feature to use in the descendent class. I like C++'s solution to this, the private/protected/public distinction. The private members of a class are only visible within the class, internal to the "black box". Protected members are visible to derived classes; frequently class derivation is used to extend behavior, in which case it is reasonable to allow a more intimate relationship between the base class and the subclasses, and protected members can be used for this. And public members are fully exported. The applicability of information hiding ideas can depend heavily on the design of the OO language. For instance, it's hard to provide this could be done in CLOS, because there is no notion of methods "belonging" to a particular class, due to its support of generic function dispatch based on the types of multiple arguments. I suppose there could be rules specifying that WITH-SLOTS and WITH-ACCESSORS may only be used on specialized parameters, and may only specify slots visible within the class named in the parameter specializer. This would support information hiding with regard to data, but it's not clear how to extend it to procedures. -- Barry Margolin, Thinking Machines Corp. barmar@think.com {uunet,harvard}!think!barmar
tynor@hydra.gatech.edu (Steve Tynor) (01/26/91)
>In article <1991Jan23.224203.3206@runx.oz.au> chrisv@runx.oz.au (Chris Velevitch) writes: >>I disagree with allowing a class to access inherited features that are >>not exported. It does not make sense that secret features are known to >>heir of a class. If a feature is not publicly known, then how can you >>know about the feature to use in the descendent class. > >I like C++'s solution to this, the private/protected/public distinction. >The private members of a class are only visible within the class, internal >to the "black box". Protected members are visible to derived classes; >frequently class derivation is used to extend behavior, in which case it is >reasonable to allow a more intimate relationship between the base class and >the subclasses, and protected members can be used for this. And public >members are fully exported. In practice, I've found C++ `private's to be overly restrictive and in violation of the rule "it is not the buisiness for a class to decide how it may be extended in the future" (paraphrased from OOSC). It's another example of a C++ feature that actually reduces the reusability of C++ classes (non-virtual member functions being the other obvious one). =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= C++: Just say no. Steve Tynor Georgia Tech Research Institute Artificial Intelligence Branch tynor@prism.gatech.edu
bertrand@eiffel.UUCP (Bertrand Meyer) (01/26/91)
From <1991Jan23.224203.3206@runx.oz.au> by chrisv@runx.oz.au (Chris Velevitch): > I disagree with allowing a class to access inherited features that are > not exported. It does not make sense that secret features are known to > heir of a class. If a feature is not publicly known, then how can you > know about the feature to use in the descendent class. > > Eiffel allows a class to be designed so it has no interface, which can > then be used in descendent classes. What use is a black box in which you > cannot put anything in or take anything out. This has come up before, of course. There is little to add to Steve Tynor's response. Let me, however, reproduce an extract from a paper called ``Static Typing in Eiffel'', of which a preliminary version was posted about two years ago. The current version is part of a book called ``An Eiffel Collection'', which is a collection of Eiffel-related articles, published by Interactive. The text is an ASCII-equivalent of something meant for typesetting, with heavy use of italics, boldface and some graphics. The general ideas, however, should survive e-mail translation. - -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- Extracted from ``Static Typing for Eiffel'' In: An Eiffel Collection Published by Interactive Software Engineering, 1991. Reference TR-EI-20/EC. Copyright B. Meyer, 1991 - -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 4.1 Rationale for the export rule Consider first the export-inheritance rule, which often surprises newcomers as an apparent violation of information hiding principles. How can a descendant override export restrictions of an ancestor, or hide what the ancestor exported? To obtain a better perspective on this rule it is useful to examine its application to what may be the archetypal example of inheritance. Assume PARENT is a class POLYGON and HEIR is a class RECTANGLE. The inheritance relation in this case is clear and natural. It is not absurd, however, to assume that class POLYGON has a procedure add_vertex (new: POINT) is -- Insert new vertex new at current cursor position do ... ensure nb_vertices = old nb_vertices + 1 end -- add_vertex For general polygons, this procedure may make sense. It is not, however, applicable to rectangles, whose class invariant should contain the clause nb_vertices = 4 The Eiffel solution is to ensure that class RECTANGLE does not export procedure add_vertex. This is the kind of case that led to situations such as [REFERENCE TO EARLIER EXAMPLE OF ERRONEOUS FEATURE CALL OF THE FORM x.f], with f being add_vertex. |- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- ---| | | | | | (Sorry, no graphics available for e-mail) | | | |(Please draw a figure with a bubble labeled POLYGON at the center | |top; below it, bubbles labeled FIXED_POLYGON and VARIABLE_POLYGON, | |with a single arrow from each of these bubbles to the top one; | |below FIXED_POLYGON, a bubble labeled RECTANGLE, with a bubble | |to FIXED_RECTANGLE.) | | | | | | Figure 1: Alternative inheritance structures | | | - -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -| Three policies are possible in the face of such cases. + Policy 1 considers that this use of inheritance is inadequate: RECTANGLE is not a subtype of POLYGON because not all operations applicable to the latter are applicable to the former. According to this policy, the proper solution (see figure 1) is to consider two heirs to POLYGON: FIXED_POLYGON and VARIABLE_POLYGON. Procedure add_vertex is a feature of VARIABLE_POLYGON only, whereas RECTANGLE inherits from FIXED_POLYGON. This way the Eiffel rule can be changed to require that every class should export all features exported by its ancestors. (Even with this policy, however, there is no reason to prohibit a class from exporting a feature hidden by a proper ancestor. This technique is commonly used in practical Eiffel programming and, as discussed above, entails no danger of type failure.) + Policy 2 considers that inheritance is used properly in this case, but only for half of its capacity: as a module enrichment mechanism, not as a subtyping facility. The definition of class RECTANGLE should be permitted, but not polymorphic assignments of the form p := r (with p of type POLYGON and r of type RECTANGLE). With such a policy the conformance rules governing such assignments, as outlined in section 11.3.1 of ``Object-Oriented Software Construction'' (Prentice- Hall, 1988) and given fully in ``Eiffel: The Language'' (Interactive Software Engineering, 1989, and Prentice-Hall, 1991), would have to be updated to exclude such cases. If polymorphic assignments are needed, then the inheritance structure should be redesigned as shown in figure 1. + Policy 3 is more liberal: it permits the above polymorphic assignments as long as it can be ascertained statically at reasonable cost, for any Eiffel system involving these classes, that no type failure may result; in other words, that no add_vertex operation may ever be applied to p or an entity that may become associated with p. This is the policy applied in Eiffel, for which the exact rules will be given below. What speaks in favor of policies 1 and 2 is that they are extremely easy to implement for the compiler writer. Restricting exports in descendant classes (policy 1) is trivial; restricting polymorphic aliasing (policy 2) is almost as immediate. These solutions would also have the advantage of quieting the theoreticians and removing the fears of prospective users. In short, all obvious arguments seemed to push the designers of Eiffel towards policy 1 or 2: convenience (since the same group is also in charge of an implementation) and ease of convincing future users. So it takes some dose of fortitude to choose policy 3 and stick to it. Why should this be the Eiffel policy? The reasons can only be understood by going beyond the purely theoretical discussions and considering the practice of object-oriented software construction. Policies 1 and 2 assume that programming is for gods. Gods never make any mistake. Those among them who aside from their other business indulge in the heavenly pleasures of object- oriented programming always get their inheritance structures right the first time around. In today's job market, however, most employers must resort to hiring programmers who are only demigods, or even in some cases (although they will deny it) mere mortals. Then we have to accept the need to work with inheritance structures that may already in be place and have been designed with less-than-perfect foresight. Assume for example that POLYGON has already been designed and has descendants such as CONVEX_POLYGON and CONCAVE_POLYGON. In other words, the existing inheritance structure does not take into account the difference between ``variable'' and ``fixed'' polygons. Then a new programmer comes in and has a need for RECTANGLE; writing it as an heir to POLYGON seems the obvious solution. Policy 1 precludes doing this without first reorganizing the entire inheritance structure. This is unrealistic in a practical development environment. True, we should accept the need for regular improvements of inheritance structures, reflecting improved understanding of software artefacts. But as anyone who has managed a class library in an industrial environment will appreciate, such evolution should be carefully planned and properly controlled. One cannot accept a situation in which new but legitimate uses of a class require constant changes in the design of existing libraries. Policy 2 is only marginally better. It does allow the programmer to define RECTANGLE as an heir to POLYGON but prevents him from using polymorphism in this case; it is not possible, for example, to define a data structure such as polygon_stack: STACK [POLYGON] and push a RECTANGLE object onto polygon_stack. This is particularly frustrating in an application that never even uses add_vertex. Again, the only solution is to redesign the inheritance structure. Policies 1 and 2 are overly inflexible. In contrast, the idea of policy 3 is to avoid bothering programmers with unnecessary restrictions when their use of inheritance cannot possibly lead to any type failure. The criterion to apply is obvious in the example given: the checker should reject any software system that contains both of the operations + p := r + p.add_vertex (...) Because checkers cannot realistically carry out flow analysis, it is not necessary to check whether or not these two instructions can be executed on the same control flow path; the mere presence of both in the same system is reason enough for the checker to reject that system. In other words, we accept without regret that the following sequence (with n an integer entity) will be rejected even though it cannot possibly lead to a type failure: if n >= 0 then p := r end if n < 0 then p.add_vertex (...) end The reason for this was discussed in [AN EARLIER] section [OF THE PAPER]: checkers should only be bound to perform ``reasonable'' controls. For a checker to recognize that the above is safe requires control flow analysis, whose cost, if it is at all possible, is not justified by the benefit. Using the terminology of [AN EARLIER] section, we are past the point of diminishing returns. As noted [EARLIER], type checking is always a pessimistic strategy; the only question is what degree of pessimism is acceptable. Rejecting extracts of the above form appears acceptable because it is hard to conceive of them being used in useful programs. This is not true, however, of the much stronger pessimism implied by policies 1 and 2, which leads to rejection of programs that, for the reasons discussed above, are useful and even needed. How policy 3 can be implemented at reasonable cost will be discussed [IN LATER SECTIONS]. 4.2 Further reflections on inheritance The argument made above for orthogonal export and inheritance mechanisms was that inheritance structures may be temporarily imperfect. The implied consequence is that eventually these structures will be perfected; then policies 1 and 2 could be implemented if we were willing to implement tougher library management procedures (forcing restructuring when appropriate, and requiring new heir designs to wait in the meantime). Unfortunately, even such an approach may not be viable. In practice we must probably accept that some inheritance structures will always be imperfect. Inheritance is the application to software engineering of the most fundamental of all scientific techniques: classification. The modern forms of the natural sciences, since Linnaeus, and of mathematics, at least since Bourbaki, were borne out of systematic efforts to classify natural and artificial objects; object-oriented programming attempts the same for software objects. Classification is humanity's attempt to bring a semblance of order to the description of an otherwise rather chaotic world. As anyone who has ever tried to use a botany book to recognize actual flowers knows, classifications never quite achieve perfection. Close as one can get to a fully satisfactory system, a few exceptions and special examples will remain. Moving from botany to zoology, the cliche' example of ostriches and flying illustrates these problems well. In class BIRD, it seems appropriate to include and export a feature ``fly''. But a descendant class such as STRUTHIO (the name of the genus that includes ostriches) should not export ``fly''. This is really the Eiffel form of selective inheritance, or rejecting part of your heritage: the rejected feature is still there internally, but not visible by your clients. (The discussion of selective inheritance in section 10.5.3 of ``Object-Oriented Software Construction'' is too restrictive in this respect: it seems to reject any form of selective inheritance, even though Eiffel has always supported the form discussed here.) In a case such as this one there does not seem to be a much better solution. Any other classification of birds, for example into flying and non-flying ones, would miss key criteria for distinguishing between various classes of birds, and would undoubtedly cause bigger problems than the original solution. Multiple inheritance from a class FLYING_THING would not help much. So far, only a minority of Eiffel users have admittedly reported ostrich-oriented programming as their major area of technical interest. But the need to deal with imperfect inheritance structures seems universal. Of course, we should strive to make these structures as complete and regular as possible. But we must also accept that unexpected special cases and exceptions may always occur. When they do and the programmer is only performing safe manipulations, the programming environment should help him, not put undue restrictions in his way. - -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- End of extract - -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- Bertrand Meyer bertrand@eiffel.com
paj@mrcu (Paul Johnson) (01/28/91)
>In article <1991Jan23.224203.3206@runx.oz.au> chrisv@runx.oz.au (Chris Velevitch) writes: >>I disagree with allowing a class to access inherited features that are >>not exported. It does not make sense that secret features are known to >>heir of a class. If a feature is not publicly known, then how can you >>know about the feature to use in the descendent class. > >I like C++'s solution to this, the private/protected/public distinction. I think the idea is that a child should be able to see all of its parents attributes so that the implementation is not constrained by the decisions of the parents' implementors. If someone decides to construct a class which inherits from a single parent simply to export more attributes then that is their buisness. They have designed and built a new class. If the interface is bad, or the invarients no longer hold or some similarly idiotic thing then that is the programmer's fault for producing a bad design. Eiffel is not a bondage and discipline language, and like any good language it will give the programmer plenty of rope. If he hangs himself, well he should have taken more care and heeded the safty warnings. The big advantage of Eiffel over C++ is that thanks to conceptual simplicity, all the dangerous things in Eiffel can be clearly signposted. C++ forces you to take the blade-guards off in order to use it. The private/protected/public system is just sugar and I cannot see how it helps anyone. I can see how it would get in their way though. Paul. -- Paul Johnson UUCP: <world>!mcvax!ukc!gec-mrc!paj --------------------------------!-------------------------|------------------- GEC-Marconi Research is not | Telex: 995016 GECRES G | Tel: +44 245 73331 responsible for my opinions. | Inet: paj@uk.co.gec-mrc | Fax: +44 245 75244
chip@tct.uucp (Chip Salzenberg) (01/28/91)
According to rick@tetrauk.UUCP (Rick Jones): >In article <1991Jan24.214652.18515@Think.COM> barmar@think.com (Barry Margolin) writes: >>I like C++'s solution to this, the private/protected/public distinction. >>The private members of a class are only visible within the class, internal >>to the "black box". Protected members are visible to derived classes ... >>And public members are fully exported. > >The problem I see with this approach is that the designer of a class has to >anticipate every possible way in which his class may be used by another >programmer. I'm sorry, but I don't believe that any method of code reuse -- whether inheritance, delegation or "editor block-copy subroutines" -- will ever be able to guarantee 100% reuse *without modification*. The C++ public/protected/private specifications are a concise way for the programmer of a class to express his expectations as to future derivation from the given class. If a second programmer thinks that she needs to get at a private member of such a class, that is a hint that the base class's author didn't anticipate the second programmer's intended use of the class. It will imply to a good programmer that either (1) the derivation in question is a Bad Idea, or (2) the base class requires some adjustment before the derivation will work. I don't see how eliminating the "private" keyword would fix this basic difficulty with deriving from another programmer's classes. -- Chip Salzenberg at Teltronics/TCT <chip@tct.uucp>, <uunet!pdn!tct!chip> "If Usenet exists, then what is its mailing address?" -- me "c/o The Daily Planet, Metropolis." -- Jeff Daiell
chip@tct.uucp (Chip Salzenberg) (01/28/91)
According to tynor@hydra.gatech.edu (Steve Tynor): >In practice, I've found C++ `private's to be overly restrictive and in >violation of the rule "it is not the buisiness for a class to decide how >it may be extended in the future" (paraphrased from OOSC). An interesting "rule" -- one with which I happen to disagree. In any case, language features don't stand up and say, "Use me!" Who can say that there will never be good reason to make members private? Not I -- even though I use "protected" five times more often than I use "private". >It's another example of a C++ feature that actually reduces the reusability >of C++ classes (non-virtual member functions being the other obvious one). Virtual functions are available when appropriate; but I'm just as glad that I need not pay the performance penalty when I don't need them. -- Chip Salzenberg at Teltronics/TCT <chip@tct.uucp>, <uunet!pdn!tct!chip> "If Usenet exists, then what is its mailing address?" -- me "c/o The Daily Planet, Metropolis." -- Jeff Daiell
linton@sgi.com (Mark Linton) (01/30/91)
In article <27A44871.5586@tct.uucp>, chip@tct.uucp (Chip Salzenberg) writes: |> According to tynor@hydra.gatech.edu (Steve Tynor): |> >In practice, I've found C++ `private's to be overly restrictive and in |> >violation of the rule "it is not the buisiness for a class to decide how |> >it may be extended in the future" (paraphrased from OOSC). |> |> An interesting "rule" -- one with which I happen to disagree. |> |> In any case, language features don't stand up and say, "Use me!" Who |> can say that there will never be good reason to make members private? |> Not I -- even though I use "protected" five times more often than I |> use "private". |> |> >It's another example of a C++ feature that actually reduces the reusability |> >of C++ classes (non-virtual member functions being the other obvious one). |> |> Virtual functions are available when appropriate; but I'm just as glad |> that I need not pay the performance penalty when I don't need them. Private is for things that might change. If you make them protected and then change them, you break subclasses. So if you're not willing to commit to keeping something around, then you should make it private. After starting with the attitude that I should make things by default protected, I now believe the appropriate default is private. If I make something available to a subclass, I should think carefully about what I am giving out. As for virtual functions, if you can't live with cost of a virtual function call you are unlikely to be happy with the cost of a direct function call. A virtual function call typically adds a few memory references on top of what is likely a considerably more expensive operation, involving register save/restore and a branch.
chrisv@runx.oz.au (Chris Velevitch) (01/30/91)
In article <1991Jan24.214652.18515@Think.COM> barmar@think.com (Barry Margolin) writes: >In article <1991Jan23.224203.3206@runx.oz.au> chrisv@runx.oz.au (Chris Velevitch) writes: >>I disagree with allowing a class to access inherited features that are >>not exported. It does not make sense that secret features are known to >>heir of a class. If a feature is not publicly known, then how can you >>know about the feature to use in the descendent class. > >I like C++'s solution to this, the private/protected/public distinction. >The private members of a class are only visible within the class, internal >to the "black box". Protected members are visible to derived classes; >frequently class derivation is used to extend behavior, in which case it is >reasonable to allow a more intimate relationship between the base class and >the subclasses, and protected members can be used for this. And public >members are fully exported. I can't see the real need for the protected members, because you are publicly announcing the existence of the members. You might as well just make them public. The class specifications remainings the same. I believe that by making features protected, you are making a supposition on how the class will be used. You may find that once having made it known the feature exists, a user will find a case in which they only want to buy the feature and not inherit it. -- Chris Velevitch RUNX Unix Timeshare | Internet: chrisv@runxtsa.runx.oz.au Sydney NSW, Australia. | UUCP: uunet!runxtsa.runx.oz.au!chrisv
jbuck@galileo.berkeley.edu (Joe Buck) (01/31/91)
According to tynor@hydra.gatech.edu (Steve Tynor): > >In practice, I've found C++ `private's to be overly restrictive and in > >violation of the rule "it is not the buisiness for a class to decide how > >it may be extended in the future" (paraphrased from OOSC). In article <27A44871.5586@tct.uucp>, chip@tct.uucp (Chip Salzenberg) writes: > An interesting "rule" -- one with which I happen to disagree. I also disagree. There is a tension between information hiding and inheritance, and both are valuable concepts. The ability to reuse code is very valuable, but the ability to separate interface and implementation, and to be able to, in many cases, redesign the implementation without affecting the user of a class is extremely valuable. But what the advocates of never using "private" are forgetting is that the person that derives from a class is also a user of the class -- one that is more privileged than the average client, but a client nevertheless. Chip goes on to write: > In any case, language features don't stand up and say, "Use me!" Who > can say that there will never be good reason to make members private? > Not I -- even though I use "protected" five times more often than I > use "private". "private" is very valuable for enforcing invariants: for example, assuring that certain data members will always have a certain relationship. For example, consider a class of objects that represent two-way connections. The base class has only one data member: a pointer to the object that is connected to it (a null pointer will represent no connection). We want derived classes to be able to manipulate the connection however they please, with this proviso: if A is connected to B, B is connected to A. If A is deleted, B will revert to being disconnected (we will never have a dangling pointer to a deleted object). Now, suppose we want derived classes to be able to control the type of object that is connected to it: a DerivedConnection can only be connected to another DerivedConnection. This means we don't want the raw connect methods to be generally accessible: we make the methods protected, so derived classes can build on them however they like. Here is a subset of the interface: class Connection { private: Connection * peer; // who I am connected to protected: // disconnect me if I am connected void disconnect() { if (peer) peer->peer = 0; peer = 0; } // form a new connection void connect(Connection& newPeer) { disconnect(); newPeer.disconnect(); peer = &newPeer; newPeer.peer = this; } // return who I am connected to Connection * myPeer() const { return peer;} public: Connection() : peer(0) {} ~Connection() { disconnect();} } A real implementation will have additional functions; myPeer might be public. > >It's another example of a C++ feature that actually reduces the reusability > >of C++ classes (non-virtual member functions being the other obvious one). Reusability is not the only thing we care about. Those of us building large systems also care about robustness. Notice that the above code (modulo any bugs or stupidity; I haven't checked it) enforces several assertions about connections that I can count on: these assertions can't be broken by derived classes OR by clients. Only two methods can alter a "peer" pointer. > Virtual functions are available when appropriate; but I'm just as glad > that I need not pay the performance penalty when I don't need them. Agreed. Imagine the performance drag if every function in a complex number class were virtual. C++ is not Smalltalk, and object-oriented programming is not Smalltalk programming. Ready Grady Booch's book for a lot more detail on the private/protected/public distinction. |> Chip Salzenberg at Teltronics/TCT <chip@tct.uucp>, <uunet!pdn!tct!chip> |> "If Usenet exists, then what is its mailing address?" -- me |> "c/o The Daily Planet, Metropolis." -- Jeff Daiell -- Joe Buck jbuck@galileo.berkeley.edu {uunet,ucbvax}!galileo.berkeley.edu!jbuck
jbuck@galileo.berkeley.edu (Joe Buck) (01/31/91)
In article <1991Jan29.221121.20642@odin.corp.sgi.com>, linton@sgi.com (Mark Linton) writes: > As for virtual functions, if you can't live with cost of a virtual function call > you are unlikely to be happy with the cost of a direct function call. A virtual > function call typically adds a few memory references on top of what is likely > a considerably more expensive operation, involving register save/restore and > a branch. The choice isn't always between the overhead of a virtual function call and a direct function call -- you forgot about "inline". In most cases virtual functions can't be inlined (except in cases where the compiler knows the exact class). -- Joe Buck jbuck@galileo.berkeley.edu {uunet,ucbvax}!galileo.berkeley.edu!jbuck
jbuck@galileo.berkeley.edu (Joe Buck) (01/31/91)
In article <808@puck.mrcu>, paj@mrcu (Paul Johnson) writes: |> The big advantage of Eiffel over C++ is that thanks to conceptual |> simplicity, all the dangerous things in Eiffel can be clearly |> signposted. C++ forces you to take the blade-guards off in order to |> use it. The private/protected/public system is just sugar and I |> cannot see how it helps anyone. I can see how it would get in their |> way though. Read Grady Booch's book and he'll tell you why. The private/protected/ public feature is one of his favorite things about the language, and he gives good arguments. The problem is that you need to know how to use it. -- Joe Buck jbuck@galileo.berkeley.edu {uunet,ucbvax}!galileo.berkeley.edu!jbuck
bertrand@eiffel.UUCP (Bertrand Meyer) (01/31/91)
Anyone who wants to argue that the Eiffel policy is not the appropriate one is going to have to leave pious generalities and offer serious technical arguments. Will anyone take up the simplest example (discussed in my posting <485@eiffel.UUCP>): a class POLYGON exists and has a procedure ``add_vertex'', which it exports. You want to add a class RECTANGLE. What do you do? 1. Decide not to use inheritance. No comment. 2. Redo the existing inheritance hierarchy. Of course, even assuming you are permitted to do it, this will only save you temporarily: someone else, or you a while later, will want to perform new changes based on criteria other than fixed versus variable number of vertices: for example regular versus irregular, etc. You'll soon have a total mess if you are trying to keep up with various conflicting classifications. 3. Accept that RECTANGLE inherits from POLYGON and does not export ``add_vertex''. (This procedure violates the invariant of class RECTANGLE anyway, so the consistency rules forbid it to be exported, but of course non-exported routines do not have to maintain the invariant.) To avoid burdening the compiler writer, call this ``implementation inheritance'' and disallow polymorphic assignments of the form p := r where p is of type POLYGON and r of type RECTANGLE. 4. Accept that RECTANGLE inherits from POLYGON and does not export ``add_vertex''. Permit polymorphic assignments of the above form; but have the type checker reject as invalid any system which would contain both such an assignment and a call p.add_vertex (...), since it may lead to a run-time inconsistency. Do not introduce any distinction between ``implementation inheritance'' and any other kind. Based on not inconsiderable experience with the design of O-O systems and class hierarchies, I believe that solution 4 is the only reasonable one. I would be interested to see any technical counter-argument, and alternative solutions to the problem mentioned above. Of course solution 4 causes more work for implementers, since type checking must now be done on an entire system rather than a single class. But that's what computers are for: tedious, exhaustive work, especially when (as here) the consequences of not doing that work are potentially rather damaging. In earlier articles I have described a strategy for doing the work in an incremental fashion so that its performance overhead is acceptable. As an aside (meant to be thought- rather than flame-provoking) I have come to realize, although not early enough, that information hiding is a greatly overrated goal. What counts is abstraction. Information hiding is often a convenient syntactical way to help achieve abstraction, but is a means rather than an end, and is not desirable in 100% of cases. It must always be tempered with other criteria. Just because David Parnas and many others after him (including the undersigned) have emphasized the potential benefits of information hiding in well-defined cases does not make incantation a substitute for thinking. (Another of these incantations is ``implementation inheritance''. This does not mean anything. In class POLYGON, why would ``add_vertex'' be implementation and ``access_vertex'' specification?) All too often we forget that design methodology and programming languages are meant to help people produce good software, not to restrict their creative freedom. -- -- Bertrand Meyer bertrand@eiffel.com
rick@tetrauk.UUCP (Rick Jones) (01/31/91)
Given that this thread is starting to look like an Eiffel v C++ debate, there is a significant aspect of Eiffel which has been overlooked - assertions. One of the concepts of Eiffel is that the properties of a class may (and should) be specified in terms of the class invariant and pre- and post-conditions of routines. When using inheritance to write a new class, you can access and/or redefine anything in the parent class, but you cannot override the assertions. (In the current version you can override the pre- and post-conditions, but this is a recognised compiler problem which is going to be rectified.) You certainly can't override the invariant, but you can strengthen it by adding additional invariant properties. The correct way to use Eiffel is to be rigorous with the use of assertions, and run all your tests with all assertion checking on. The assertion checking may then be turned off for a production version once you are sure that the assertions never get violated. This is a sometimes subtle but significantly different approach to the problem of protecting the semantics of classes, and avoids the need to hide the implementation details from descendant classes. You can look at it by saying that the interface of a class is public, the implementation is protected, and the assertions are private (it's not an exact analogy, but it gives the right feel). Of course you have to get your assertions correct, but I don't believe that is any harder than getting the public/private/protected bit right. I have already had several occasions to be glad of Eiffel's assertion system, since it has been able to nip in the bud a number of errors which would otherwise have gone unnoticed and become obscure time-bomb bugs. -- Rick Jones Tetra Ltd. Maidenhead, Was it something important? Maybe not Berks, UK What was it you wanted? Tell me again I forgot rick@tetrauk.uucp -- Bob Dylan
chip@tct.uucp (Chip Salzenberg) (02/01/91)
According to bertrand@eiffel.UUCP (Bertrand Meyer): >A class POLYGON exists and has a procedure >``add_vertex'', which it exports. You want >to add a class RECTANGLE. What do you do? > >1. Decide not to use inheritance. >2. Redo the existing inheritance hierarchy. >3. Accept that RECTANGLE inherits from POLYGON and does not export > ``add_vertex''. ... and disallow polymorphic assignments of the form > p := r where p is of type POLYGON and r of type RECTANGLE. >4. Accept that RECTANGLE inherits from POLYGON and does not export > ``add_vertex''. Permit polymorphic assignments of the above form; > but have the type checker reject as invalid any system which would > contain both such an assignment and a call p.add_vertex (...), since > it may lead to a run-time inconsistency. In C++, the add_vertex() member function would almost certainly be virtual; that is, it would act correctly in cases where a |POLYGON*| actually points to an object of type |RECTANGLE|. (If it were not virtual already, it could be made so by the insertion of the word "virtual" at one place in the code.) Given this language feature, add_vertex becomes a non-problem. The implementor of RECTANGLE simply provides an appropriate definition of the RECTANGLE::add_vertex(): a do-nothing function body that returns a failure indication (if appropriate). Analyzing this use of C++ results in a fifth item for Bertrand's list: 5. Accept that RECTANGLE inherits from POLYGON and exports add_vertex, and require the programmer to provide an implementation of add_vertex appropriate for a RECTANGLE. In the real world, how is this solution deficient? >As an aside (meant to be thought- rather than flame-provoking) >I have come to realize, although not early enough, that >information hiding is a greatly overrated goal. Which reminds me of a nice phrase from Leo Brodie's _Thinking_Forth_, that goes something like this: Some programmers use information hiding as if programs are prone to attack from roving bands of hostile programmers. Of course, Forth isn't exactly my favorite programming environment; I prefer driving with seatbelts (but not a padded cell). :-) -- Chip Salzenberg at Teltronics/TCT <chip@tct.uucp>, <uunet!pdn!tct!chip> "I want to mention that my opinions whether real or not are MY opinions." -- the inevitable William "Billy" Steinmetz
barmar@think.com (Barry Margolin) (02/01/91)
In article <488@eiffel.UUCP> bertrand@eiffel.UUCP (Bertrand Meyer) writes: >4. Accept that RECTANGLE inherits from POLYGON and does not export >``add_vertex''. Permit polymorphic assignments of the above form; >but have the type checker reject as invalid any system which would >contain both such an assignment and a call p.add_vertex (...), since >it may lead to a run-time inconsistency. Do not introduce >any distinction between ``implementation inheritance'' and any other >kind. > >Based on not inconsiderable experience with the design of O-O systems >and class hierarchies, I believe that solution 4 is the only reasonable >one. I would be interested to see any technical counter-argument, >and alternative solutions to the problem mentioned above. If static type checking is a requirement, I believe you are right. However, in OO systems with dynamic type checking and method dispatch, there is another solution: 5. Require that add_vertex (or, generally, all methods) be a virtual function (in languages that require such a distinction to be made). Accept that RECTANGLE inherits from POLYGON. Override add_vertex in the RECTANGLE class with a function that reports an error. Permit polymorphic assignments of the form p=r, and accept the fact that it is possible for p.add_vertex(...) to signal an error at runtime. This is not a realistic solution in a language without a decent exception handling mechanism, but that's an excuse for adding exception handling, not for crippling programs. Furthermore, some languages (e.g. CLOS) even allow the class of an object to change dynamically, so there's another solution: 6. As in 5, use virtual function inheritance of add_vertex. RECTANGLE::add_vertex would change the class of self to POLYGON and then call self.add_vertex (which will invoke the POLYGON::add_method, which might further change the class to PENTAGON). In fact, schemes like this may be the only sensible way to implement a system of this type. Consider a context in which the above classes would be used: a graphical drawing program. The user clicks on an object and then selects an operation from a menu (or vice versa). Solution 6 would be useful if you want to allow users to use the rectangle tool to create an object, but later permit them to edit it in such a way that it is no longer a rectangle. Solution 5 is useful if the UI design specifies that objects created using the rectangle tool must always stay rectangular; when the user tries to add a vertex to the object, an error message would be put up telling them that the operation is not appropriate for that object. The logic necessary to put up this warning must exist in the program somewhere, so why not embed it directly in the methods that need it? With solution 4, the code to select an object could not use polymorphic assignment, because eventually the object will be passed to the add_vertex function; it would probably have to use a discriminated union, and all future uses of that union would have to dispatch on the discriminant (isn't this what OO programming was supposed to relieve us from?). If the menu system supports "greying out" inappropriate menu items, this could be done by having a virtual list_allowed_operations or is_operation_allowed method. Either way, I think it would be very difficult to structure the program in such a way that a static type checker could verify that add_vertex is only called on POLYGONs that aren't specialized to a particular number of vertices, unless is_operation_allowed were made a primitive of the language (actually, it's a good idea to have such a primitive, because it would be easy for user-defined functions like this to get out of sync with the actual code). -- Barry Margolin, Thinking Machines Corp. barmar@think.com {uunet,harvard}!think!barmar
mario@cs.man.ac.uk (Mario Wolczko) (02/01/91)
In article <488@eiffel.UUCP>, bertrand@eiffel.UUCP (Bertrand Meyer) writes: > > Anyone who wants to argue that the Eiffel policy is not the > appropriate one is going to have to leave pious generalities > and offer serious technical arguments. and then offers some design alternatives... ...but the options he then lists are not exhaustive. Eiffel allows the programmer to control the visibility of any feature to clients via the export clause. The problem is that *all* features are visible to children. If, in the implementation of a class (especially a deferred class), the programmer needs a feature (be it a routine or attribute) for purely private use, and which can be provided in a number of different forms, then it should be possible for the programmer to say, ``Don't assume that this will be present in this form in all versions of this class'', ie that children should not be able to use this feature. That's what the protected mode of C++ is for. If the feature is protected, then the implementor of the class is free to change or remove it, provided that the advertised interfaces (to clients and children) work as advertised! An inability to do this violates what I call "the Principle of Decomposition": at any time the programmer should be free to decompose one thing into more than one thing, without affecting the advertised interface or behaviour. If you mention this in isolation, most people nod in agreement. But no mainstream OO language satisfies it! If we substitute "feature" for "thing", then Eiffel fails because of the described behaviour: the extra features appear in the subclass interface. If we substitute "class" for "thing" (ie decompose one class into two classes, related by inheritance), then *no* mainstream OO language can satisfy the principle, because one cannot localise the interaction between parent and child (self/this/current is always bound in the class of the receiver, which may be a descendant of the child). None of this is new to OOP: it's a problem in any ADT theory/language: Can you implement any ADT and not have the implementation show through? The philosophical point is whether you feel that the parent/child boundary consititutes an interface at which hiding should be possible. I do; I know Dave Ungar doesn't (he told me); I'd be interested to hear what Bertrand Meyer thinks. I think the point is well-argued in Snyder's paper [1], and I was surprised when I came across Eiffel that it did not adopt this policy (especially in view of its "software engineering" stance). I feel sure that Bertrand Meyer was aware of this paper when designing Eiffel, so perhaps he could explain his decision to do otherwise? > All too often we forget that design methodology and programming > languages are meant to help people produce good software, not > to restrict their creative freedom. Quite. I want the freedom to create classes with *exactly* the interfaces (client & child) I want. [1] Alan Snyder, Inheritance and the Development of Encapsulated Software Components, Research Directions in Object-Oriented Programming, MIT Press, 1987, pp147-64 (an earlier version appears in OOPSLA 86). Mario Wolczko ______ Dept. of Computer Science Internet: mario@cs.man.ac.uk /~ ~\ The University uucp: mcsun!ukc!man.cs!mario ( __ ) Manchester M13 9PL JANET: mario@uk.ac.man.cs `-': :`-' U.K. Tel: +44-61-275 6146 (FAX: 6280) ____; ;_____________the mushroom project___________________________________
doug@saturn.ads.com (Doug Morgan) (02/01/91)
In article <488@eiffel.UUCP> bertrand@eiffel.UUCP (Bertrand Meyer) writes:
Will anyone take up the simplest example (discussed in my
posting <485@eiffel.UUCP>): a class POLYGON exists and has
a procedure ``add_vertex'', which it exports.
You want to add a class RECTANGLE. What do you do?
1. ...
2. ...
3. ...
4. Accept that RECTANGLE inherits from POLYGON and does not export
``add_vertex''. Permit polymorphic assignments of the above form;
but have the type checker reject as invalid any system which would
contain both such an assignment and a call p.add_vertex (...), since
it may lead to a run-time inconsistency. Do not introduce
any distinction between ``implementation inheritance'' and any other
kind.
... I would be interested to see any ... alternative solutions ...
--
-- Bertrand Meyer
bertrand@eiffel.com
4. is a bit strict for my taste. I wouldn't mind it as a option,
however. I like being able to do this (say, in CLOS):
5. Accept that RECTANGLE inherits from POLYGON and *does* export
``add_vertex''. Permit polymorphic assignments of the form p := r
where p is of type POLYGON and r of type RECTANGLE and have a
subsequent call of p.add_vertex (...) convert the current RECTANGLE
implementation (and class) of object p to a POLYGON implementation
(and class).
Of course, for this example I changed the "invariants" for the
RECTANGLE class. They are not unreasonable, just beyond the
compile-time restrictions of Eiffel.
Doug Morgan
doug@ads.com
tynor@hydra.gatech.edu (Steve Tynor) (02/01/91)
>From: barmar@think.com (Barry Margolin) ... >that RECTANGLE inherits from POLYGON. Override add_vertex in the RECTANGLE >class with a function that reports an error. Permit polymorphic >assignments of the form p=r, and accept the fact that it is possible for >p.add_vertex(...) to signal an error at runtime. This is not a realistic >solution in a language without a decent exception handling mechanism, but >that's an excuse for adding exception handling, not for crippling programs. The problem with this sort of solution is that it defers detection of the error until run-time. It is very difficult to statically examine your code to determine whether your program will fail (you have to do a dataflow analysis which is hard in general). I've run into more than a few latent bugs in large Flavors and C++ programs because of this sort of behavior. [ Before someone jumps on my case telling me about C++ pure virtuals, I know about them - in the case I'm describing, they were not used appropriately by the class designer ]. I'm a big believer in doing as much static code analysis as you (or better yet, the compiler) can - I realize that it is not always feasible (eg. in dynamic systems like CLOS and Flavors). =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= "Language is a virus from outer space" - William S. Burroughs Steve Tynor Georgia Tech Research Institute Artificial Intelligence Branch tynor@prism.gatech.edu
craig@Neon.Stanford.EDU (Craig D. Chambers) (02/01/91)
In article <488@eiffel.UUCP> bertrand@eiffel.UUCP (Bertrand Meyer) writes: > > Will anyone take up the simplest example (discussed in my >posting <485@eiffel.UUCP>): a class POLYGON exists and has >a procedure ``add_vertex'', which it exports. >You want to add a class RECTANGLE. What do you do? > >3. Accept that RECTANGLE inherits from POLYGON and does not export >``add_vertex''. (This procedure violates the invariant of class >RECTANGLE anyway, so the consistency rules forbid it to be exported, >but of course non-exported routines do not have to maintain the >invariant.) To avoid burdening the compiler writer, >call this ``implementation inheritance'' and disallow >polymorphic assignments of the form p := r where p is of type >POLYGON and r of type RECTANGLE. > >4. Accept that RECTANGLE inherits from POLYGON and does not export >``add_vertex''. Permit polymorphic assignments of the above form; >but have the type checker reject as invalid any system which would >contain both such an assignment and a call p.add_vertex (...), since >it may lead to a run-time inconsistency. Do not introduce >any distinction between ``implementation inheritance'' and any other >kind. > >Based on not inconsiderable experience with the design of O-O systems >and class hierarchies, I believe that solution 4 is the only reasonable >one. I would be interested to see any technical counter-argument, >and alternative solutions to the problem mentioned above. > >Of course solution 4 causes more work for implementers, since >type checking must now be done on an entire system rather than a >single class. But that's what computers are for: tedious, exhaustive >work, especially when (as here) the consequences of not doing that >work are potentially rather damaging. In earlier articles I have >described a strategy for doing the work in an incremental fashion >so that its performance overhead is acceptable. This issue has bothered me for a while, but I've not posted about it before. Now with this challenge, I can't resist. Option 3 seems like the only reasonable solution to me. And I'm not arguing from an implementor's point of view; I'm not averse to a complex implementation if it supports a nice programming model and environment. The real problem with option 4 is that a large existing legal program can be invalidated by a single added line. Say you have a large application manipulating various shapes, including rectangles and polygons as above. Now say someone adds a single line to the program, say an assignment "p := r". If in the rest of the application there are no such assignments, but many calls on p.add_vertex, then all these calls are suddenly invalidated by the new assignment. This seems incredibly wrong. The legality of earlier add_vertex calls shouldn't depend on whether some future programmer makes an assignment "p := r"; instead, the new assignment should be marked illegal. This exactly corresponds to option 3: making rectangle inherit code from polygon, but not making rectangle a subtype of polygon. Consider the opposite scenario: In a large application, there are countless assignments of the form "p := r", but no calls of p.add_vertex. The environment could allow this to happen, and make rectangle a legal subtype of polygon, by pretending that the add_vertex feature didn't exist (after all, the application didn't need it). Then some programmer adds a call to p.add_vertex. Again, this last call should be marked in error, since earlier treatment of rectangles as subtypes of polygons has disallowed any future use of add_vertex. This approach is a variant of option 3, with the extension that unused features can be ignored when forcing one class to be a supertype of another. This feature is extra-lingual (part of the environment), but may be nice to have around to support reuse of parts of existing code (e.g. the rectangle class is reusing (inheriting from) part of the polygon class by pretending that certain unneeded operations like add_vertex aren't there). So I conclude, on the grounds of practical programming concerns and usability of the resulting language, that option 3 is far preferable to option 4. Actually, option 6 proposed by Barry Margolin (changing the class of a rectangle to a polygon after an add_vertex) may be a useful alternative in some situations. A more general, cleaner facility for changing the implementation of an object at run-time is called dynamic inheritance in Self. Use of this feature would require distinguishing interface from representation/implementation, with rectangle being a particular representation of polygon but not a distinct type. Then after the add_vertex, the representation of a polygon that was a rectangle changes, but not its type (it's still a polygon). None of these options works nicely in all conceivable situations, but instead imposes certain restrictions on use (limits what the programmer can write). I argue that the restrictions imposed by option 3 (assignments require subtype compatibility) and/or option 6 (some subclasses are not separate types) are more reasonable and likely to be acceptable in practice than those for option 4 (existing programs may be invalidated by new code). However, if the programmer needs to manipulate both rectangles and polygons and assign between them, do add_vertex's on polygons, and treat rectangles as a distinct type (have additional behavior for rectangles), then none of these static solutions works. The remaining solution is to perform dynamic type-checking (or, equivalently, conditional assignment in Eiffel) on calls of p.add_vertex to verify that p doesn't contain a rectangle, or to make a version of add_vertex for rectangles that signals a run-time error. -- Craig Chambers
rick@tetrauk.UUCP (Rick Jones) (02/01/91)
In article <488@eiffel.UUCP> bertrand@eiffel.UUCP (Bertrand Meyer) writes: > > Will anyone take up the simplest example (discussed in my >posting <485@eiffel.UUCP>): a class POLYGON exists and has >a procedure ``add_vertex'', which it exports. >You want to add a class RECTANGLE. What do you do? This thread seems to me to be exposing one of the fundamental problems of inheritance, which is the distinction between mutable and imutable objects. Given that inheritance implies both specialisation and extension, then if objects are considered fixed once created there is no problem: a rectangle is clearly a descendant of polygon, since it is a specialised version. Everywhere that a fixed polygon may be used, then a fixed rectangle may be used, so a rectangle is type conformant with a polygon. However, when we allow objects to be modified we are seriously challenging the whole concept of taxonomy. In fact in the shapes example, the type conformance appears to be inverted. In terms of modifying vertices, the polygon is type conformant with a rectangle. Both the CLOS solution of dynamically changing the object's class, and Bertrand Meyer's solution of extended compiler type checking, seem to be ultimately trying to address this problem. Both have their advantages and drawbacks. One problem I see with allowing the class to change is that you might not want this to happen inadvertently. Bertrand's solution covers a more general aspect of what might be called "partial inheritance", but I can see the sort of errors the compiler might generate proving very frustrating for programmers, and lead to all sorts of ugly cross-assignments being used in order to keep the compiler quiet. This sort of code could also well appear pointless on first reading, and confuse novice programmers (this is speculation, since I have not yet had to live with such a compiler). Has anyone tried to address this problem by considering that a class essentially has two interfaces? One - the "read" interface - provides information about the object in its current (abstract) state. The other - the "write" interface - offers facilities to change the object. It seems that if the language supported the ability to define not only the class TYPE of a variable, but also the class USAGE (read or write), then the restrictions would be clearer, both to the compiler and to the programmer. It's a little like the C++ "const" qualifier, but taking the semantic implications a lot further. Eiffel would be well suited to handle this notion, since it already distinguishes between functions/attributes and procedures. The concept of declaring an Eiffel reference as "read-only" would simply prevent it being used to call procedures of the class - only access to attributes and functions would be allowed (good Eiffel design avoids the use of functions which have side effects on the the object's abstract state). This is not a substitute for Bertrand's more general global type-checking, but would make code clearer in representing the current use of the object, and catch errors earlier. I believe it could also simplify the global typing problem, since read-only variables would already be limited as to the range of features available to them, but I haven't had time to consider this in any depth. Have any of the theorists out there done any work along these lines? -- Rick Jones Tetra Ltd. Maidenhead, Was it something important? Maybe not Berks, UK What was it you wanted? Tell me again I forgot rick@tetrauk.uucp -- Bob Dylan
sakkinen@tukki.jyu.fi (Markku Sakkinen) (02/01/91)
In article <27A86309.281A@tct.uucp> chip@tct.uucp (Chip Salzenberg) writes: >According to bertrand@eiffel.UUCP (Bertrand Meyer): >>A class POLYGON exists and has a procedure >>``add_vertex'', which it exports. You want >>to add a class RECTANGLE. What do you do? >> >>1. Decide not to use inheritance. >>2. [...] >> ... >In C++, the add_vertex() member function would almost certainly be >virtual; that is, it would act correctly in cases where a |POLYGON*| >actually points to an object of type |RECTANGLE|. (If it were not >virtual already, it could be made so by the insertion of the word >"virtual" at one place in the code.) Sorry, that was not the difficulty. On the contrary, in Eiffel, Smalltalk, and many other OOPL's _all_ procedures are virtual in the Simula/C++ sense. >Given this language feature, add_vertex becomes a non-problem. The >implementor of RECTANGLE simply provides an appropriate definition of >the RECTANGLE::add_vertex(): a do-nothing function body that returns a >failure indication (if appropriate). This is only possible if either: 1. 'add_vertex' was originally declared with a return value or parameter that indicates success or failure. One would not like to add such parameters to the majority of procedures, because they tend to complicate the code too much. 2. The language has an exception mechanism; incidentally, Eiffel has it but C++ hasn't - well, there is a suggestion in the Ellis & Stroustrup book. >Analyzing this use of C++ results in a fifth item for Bertrand's list: > >5. Accept that RECTANGLE inherits from POLYGON and exports add_vertex, > and require the programmer to provide an implementation of add_vertex > appropriate for a RECTANGLE. > >In the real world, how is this solution deficient? The defect of this solution is that _all_ inapproriate invocations of 'add_vertex' are detected only at run time. Some other solutions make it possible to classify the invocations into three classes at compile time: 1. Those that are certainly correct. 2. Those that are certainly wrong and cause an error message. 3. Those that may be right or wrong and need a run-time check. The bad news is that one must limit flexibility and polymorphism if one wants to avoid run-time checks completely. (I'll return to this in another posting.) Markku Sakkinen Department of Computer Science and Information Systems University of Jyvaskyla (a's with umlauts) PL 35 SF-40351 Jyvaskyla (umlauts again) Finland SAKKINEN@FINJYU.bitnet (alternative network address)
jacob@gore.com (Jacob Gore) (02/02/91)
/ comp.lang.eiffel / bertrand@eiffel.UUCP (Bertrand Meyer) / Jan 30, 1991 / > Will anyone take up the simplest example (discussed in my > posting <485@eiffel.UUCP>): a class POLYGON exists and has > a procedure ``add_vertex'', which it exports. > You want to add a class RECTANGLE. What do you do? As long as we combine the subtyping (specification) hierarchy with implementation reuse hierarchy (I'm not saying it's a bad thing, although I'm beginning to seriously wonder), the option most obvious to me is: 5. RECTANGLE needs to replace the 'add_vertex' it inherits with one that does nothing. I don't see why RECTANGLE needs to welch on the contract of its superclass in the simplest case, and I don't think it should. However, if POLYGON's 'add_vertex' contains in its contract the postcondition "num_vertices = num_vertices + 1", there is no way RECTANGLE can uphold the contract. So, I see Bertrand's reasoning behind his option 4, but I am wondering if there is a fundamentally different approach to the whole thing... I'd also like to refer people on a rather good letter on this subject, one that demonstrates what its author sees as a fundamental problem (though without offering a concrete solution): Paul W. Abrahams. "Subject: Objectivism" (in ACM Forum). Communications of the ACM, Vol. 34, No. 1, Jan. 1991, pp. 15-16. Jacob -- Jacob Gore Jacob@Gore.Com boulder
chip@tct.uucp (Chip Salzenberg) (02/02/91)
According to rick@tetrauk.UUCP (Rick Jones): >Given that this thread is starting to look like an Eiffel v C++ debate, there >is a significant aspect of Eiffel which has been overlooked - assertions. Please don't cloud discussions with irrelevant issues. Inheritance and information hiding issues are orthagonal to assertion issues. A given language may support the former, the latter, both, or neither. Let's keep the discussions clear. -- Chip Salzenberg at Teltronics/TCT <chip@tct.uucp>, <uunet!pdn!tct!chip> "I want to mention that my opinions whether real or not are MY opinions." -- the inevitable William "Billy" Steinmetz
bertrand@eiffel.UUCP (Bertrand Meyer) (02/02/91)
From <27A86309.281A@tct.uucp> by chip@tct.uucp (Chip Salzenberg): > >A class POLYGON exists and has a procedure > >``add_vertex'', which it exports. You want > >to add a class RECTANGLE. What do you do? > > In C++, the add_vertex() member function would almost certainly be > virtual; that is, it would act correctly in cases where a |POLYGON*| > actually points to an object of type |RECTANGLE|. (If it were not > virtual already, it could be made so by the insertion of the word > "virtual" at one place in the code.) [As an aside to clarify any misunderstanding: in Eiffel all routines are by default dynamically bound (``virtual''). Applying static binding when appropriate is the compiler's job, not the programmer's.] > Given this language feature, add_vertex becomes a non-problem. The > implementor of RECTANGLE simply provides an appropriate definition of > the RECTANGLE::add_vertex(): a do-nothing function body that returns a > failure indication (if appropriate). A non-problem? For a routine which is supposed to add a vertex, to do nothing at all is a non-problem? The postcondition for add_vertex says ensure number_of_vertices = old number_of_vertices + 1 This is the contract that any implementation must satisfy - or else (more on the ``or else'' below). Doing nothing is a gross violation of this contract. Of course Mr. Salzenberg's solution is not just to ``do nothing'' but to return a ``failure indication''. Returning a failure indication is not that trivial (especially if we are talking about a function that already has a result), but it can be done (if only by adding a global variable), so let's accept it. But then we have changed the contract! The postcondition is now ensure failure_indication or else (number_of_vertices = old number_of_vertices + 1) which means that the specification of the ORIGINAL ``add_vertex'' routine has now changed! The postcondition has become weaker, which is the reverse of what is required if existing clients are to be kept correct. As a result, every piece of code ever written that included a call to the old ``add_vertex'' must be updated - just because someone added the notion or rectangle somewhere. Is that object-oriented design? Is that the best one can do in C++? To be sure, a more acceptable of Mr. Salzenberg's scheme is possible. Instead of a ``do-nothing function body that returns a failure indication'', one may implement ``add_vertex'' in RECTANGLE so that it raises an exception - if the language supports exceptions, of course. This can readily be done in Eiffel; in fact, the theoretical and practical role of an exception is precisely to signal that a routine is unable to fulfil its contract. This could have been solution number 5 in the original list. With this approach, there is no need to change existing client software. But the effect is essentially to renounce static type checking for run-time checks, an unpleasant perspective. All this confirms that, as Rick Jones recently commented here, one cannot pursue this discussion properly without some reference to the specification of software elements. Assertions serve that purpose. Without assertions, you lose any perspective of the reason why a certain piece of code does what it does, and for problems such as the one discussed here there is the ever present risk of falling into mere hacking. -- Bertrand Meyer bertrand@eiffel.com
brucec@phoebus.labs.tek.com (Bruce Cohen;;50-662;LP=A;) (02/02/91)
In article <1991Jan31.185534.24530@Think.COM> barmar@think.com (Barry Margolin) writes: > ... > Furthermore, some languages (e.g. CLOS) even allow the class of an object > to change dynamically, so there's another solution: > > 6. As in 5, use virtual function inheritance of add_vertex. > RECTANGLE::add_vertex would change the class of self to POLYGON and then > call self.add_vertex (which will invoke the POLYGON::add_method, which > might further change the class to PENTAGON). > > In fact, schemes like this may be the only sensible way to implement a > system of this type. Consider a context in which the above classes would > be used: a graphical drawing program. The user clicks on an object and > then selects an operation from a menu (or vice versa). Solution 6 would be > useful if you want to allow users to use the rectangle tool to create an > object, but later permit them to edit it in such a way that it is no longer > a rectangle. There was a lengthy and somewhat inconclusive discussion in this newsgroup a few months ago of just this scheme in relation to using Self for a graphic system. I started it by noting that Self allows this technique in a graphic system (I hadn't had any real experience with CLOS at the time, especially the MetaObject Protocol). As mentioned then, there are additional benefits to changing the inheritance of an object dynamically: tailoring the space and/or time complexity of operations on the object to match its current needs (e.g., rectangles which are orthogonal to the coordinate axis can be drawn faster than other polygons on many systems). Structure objects (graphic container objects with semantics about how they display their contained graphics) can change their semantics easily. This is handy for modifying the rendering of contained objects in job lots. Most of the benefits are a result of the size and dynamic complexity of graphics, especially 3D photorealistic graphics. It's not at all uncommon to have a system containing hundreds of thousands or millions of primitive graphic objects, arranged in many complex structures. Space and time savings at rendering time become very important in these systems; it's usually a good tradeoff to lose some time in dynamically modifying the objects in an editing phase. -- ------------------------------------------------------------------------ Speaker-to-managers, aka Bruce Cohen, Computer Research Lab email: brucec@tekchips.labs.tek.com Tektronix Laboratories, Tektronix, Inc. phone: (503)627-5241 M/S 50-662, P.O. Box 500, Beaverton, OR 97077
bertrand@eiffel.UUCP (Bertrand Meyer) (02/02/91)
From <1991Feb1.015749.10111@Neon.Stanford.EDU> by craig@Neon.Stanford.EDU (Craig D. Chambers): > > > >a class POLYGON exists and has > >a procedure ``add_vertex'', which it exports. > >You want to add a class RECTANGLE. What do you do? > > > >3. Accept that RECTANGLE inherits from POLYGON and does not export > >``add_vertex''. [...] To avoid burdening the compiler writer, > >call this ``implementation inheritance'' and disallow > >polymorphic assignments of the form p := r where p is of type > >POLYGON and r of type RECTANGLE. > > > >4. Accept that RECTANGLE inherits from POLYGON and does not export > >``add_vertex''. Permit polymorphic assignments of the above form; > >but have the type checker reject as invalid any system which would > >contain both such an assignment and a call p.add_vertex (...), since > >it may lead to a run-time inconsistency. Do not introduce > >any distinction between ``implementation inheritance'' and any other > >kind. > > Option 3 seems like the only reasonable solution to me. And I'm not > ... > The real problem with option 4 is that a large existing > legal program can be invalidated by a single added line. > > Say you have a large application manipulating various shapes, > including rectangles and polygons as above. Now say someone adds a > single line to the program, say an assignment "p := r". If in the > rest of the application there are no such assignments, but many calls > on p.add_vertex, then all these calls are suddenly invalidated by the > new assignment. This seems incredibly wrong. The legality of earlier > add_vertex calls shouldn't depend on whether some future programmer > makes an assignment "p := r"; instead, the new assignment should be > marked illegal. There is a point here. But you may look at it in the following way. What is illegal is the presence in the same system of both of the instructions [A] p := r [B] p.add_vertex The error may be in one or in the other. In the situation described by Mr. Chambers, assume we have an incremental compiler, and a lot of existing code uses [B]. If someone tries to add [A] to the system, then this will be flagged by the incremental compiler, which will point at the most obvious source of the problem: the new occurrence of [A], not the existing occurrences of [B]. This invalidates the addition, not the existing code, which is what Mr. Chambers wants. In this case we get the effect of solution 3, but with much less burden on the programmer because many cases which solution 3 would flag as invalid will be accepted since they don't raise any problem. My impression is that uniformly rejecting the cases which satisfy 4 but not 3 would significantly restrict the programmer's power of expression, and that they are important and useful in practice. -- -- Bertrand Meyer bertrand@eiffel.com
amanda@iesd.auc.dk (Per Abrahamsen) (02/03/91)
Sorry to interrupt, but why does nobody accept Bertrand Meyers first
two solutions?
>>>>> On 1 Feb 91 16:30:48 GMT, jacob@gore.com (Jacob Gore) said:
Jacob> However, if POLYGON's 'add_vertex' contains in its contract the
Jacob> postcondition "num_vertices = num_vertices + 1", there is no
Jacob> way RECTANGLE can uphold the contract.
But in that case a RECTANGLE is not, conceptually, a POLYGON. There
are then two possibilities (as Bertrand Meyer mentioned in
<488@eiffel.UUCP>:
1) The postcondition in correct: You can always add a vertex to a
polygon.
In this case, a rectangle is not a polygon, and it is misleading to
use inheritance.
2) The postcondition is wrong. You can only add a vertex to some
kinds of polygons.
If the postcondition is wrong, it should be fixed, like any other
bug!
In a program which should be read and understood by other people
later, these are the only long term solutions. However, when you are
creating a throw away prototype, inheritance might be useful
nonetheless. In this case, it does not matter much whether the
language provides "implementation inheritance (solution 3), "extended
type check" (solution 4), or "run time check" (solution 5), except
that (5) is the most flexible, and (3) the most safe.
craig@Neon.Stanford.EDU (Craig D. Chambers) (02/04/91)
In article <1081@tetrauk.UUCP> rick@tetrauk.UUCP (Rick Jones) writes: >Has anyone tried to address this problem by considering that a class >essentially has two interfaces? One - the "read" interface - provides >information about the object in its current (abstract) state. The other - the >"write" interface - offers facilities to change the object. It seems that if >the language supported the ability to define not only the class TYPE of a >variable, but also the class USAGE (read or write), then the restrictions would >be clearer, both to the compiler and to the programmer. It's a little like the >C++ "const" qualifier, but taking the semantic implications a lot further. I've noticed the same read-only v. read-write interface problem. It frequently arises in parameterized data types like collections, in which the read-only interfaces to two differently instantiated collections are subtype-related in the same way that the instantiating types are related (read-only-collection[S] <= read-only-collection[T] iff S <= T, where "<=" means "is a subtype of"), the two write-only interfaces are related in the opposite way, and consequently the two read-write interfaces are unrelated. I described this situation in a posting to comp.object a while back. I've been working on a new o-o language design and have been thinking about ways to allow the programmer to specify "sub-interface" indications for each member, much in the same way that public v. private subinterfaces are distinguished in many languages. Then uses of the type (e.g. type declarations) could also specify a subinterface, such as the "read-only" subinterface of a collection. The present polygon/rectangle example could benefit from subinterfaces, since the add_vertex operation could be qualified with the "resizable" subinterface. I'm concerned a bit about achieving the desired syntactic conciseness; "read-only array" is a lot longer than "array". Maybe the compiler could somehow infer the right subinterface. Of course, subinterfaces aren't strictly required, since each subinterface could just be a separate type/class. However, subinterfaces would work well with hierarchies of e.g. collections each with similar subinterfaces, while the manual method would quickly get complicated and hard to maintain. So subinterfaces seem like a qualitative increase in expressive power. If they could be integrated with the already-present public vs. private subinterfaces, then no new language concepts would be needed (just an existing concept generalized). >Eiffel would be well suited to handle this notion, since it already >distinguishes between functions/attributes and procedures. The concept of >declaring an Eiffel reference as "read-only" would simply prevent it being used >to call procedures of the class - only access to attributes and functions would >be allowed (good Eiffel design avoids the use of functions which have side >effects on the the object's abstract state). This sounds like you're proposing a built-in read-only subinterface much like const in C++. I would like the various subinterfaces to be completely user-defined, without any notion of a read-only subinterface embedded in the language. Also, in some cases a function-only subinterface as you suggest could still have the same subtype problems; the question is whether the types of the arguments to the function are supertypes of the argument types of overridden function in the superclass (i.e. the contravariance property of function subtyping). >This is not a substitute for >Bertrand's more general global type-checking, but would make code clearer in >representing the current use of the object, and catch errors earlier. Bertrand's type-checking rules are *less general* than subinterfaces, since what his rule really accomplishes is removing features that are *never* called from the interfaces of classes (and treating inheritance links as subtype links only if assignments of the subclass to the superclass are actually made; this half of the rule doesn't come into play here). With subinterfaces, however, different parts of a class can be ignored in some cases and used in others. For example, the add_vertex feature of polygons could be in a special subinterface marked "resizable", and rectangles could be declared as a subtype of the non-resizable subinterface of polygons. Then "p1 := r" assignments and "p2.add_vertex" calls could appear in the same program, as long as p1 was declared to be the non-resizable subinterface of polygons, and p2 was declared as the resizable interface. This is not possible with Bertrand's type-checking rules. -- Craig Chambers
paj@mrcu (Paul Johnson) (02/04/91)
In article <1081@tetrauk.UUCP> rick@tetrauk.UUCP (Rick Jones) writes: >In article <488@eiffel.UUCP> bertrand@eiffel.UUCP (Bertrand Meyer) writes: >> >> Will anyone take up the simplest example (discussed in my >>posting <485@eiffel.UUCP>): a class POLYGON exists and has >>a procedure ``add_vertex'', which it exports. >>You want to add a class RECTANGLE. What do you do? > > [... Much good stuff deleted ...] > >Has anyone tried to address this problem by considering that a class >essentially has two interfaces? One - the "read" interface - >provides information about the object in its current (abstract) >state. The other - the "write" interface - offers facilities to >change the object. It seems that if the language supported the >ability to define not only the class TYPE of a variable, but also the >class USAGE (read or write), then the restrictions would be clearer, >both to the compiler and to the programmer. It's a little like the >C++ "const" qualifier, but taking the semantic implications a lot >further. > >Eiffel would be well suited to handle this notion, since it already >distinguishes between functions/attributes and procedures. The >concept of declaring an Eiffel reference as "read-only" would simply >prevent it being used to call procedures of the class - only access >to attributes and functions would be allowed (good Eiffel design >avoids the use of functions which have side effects on the the >object's abstract state). This is not a substitute for Bertrand's >more general global type-checking, but would make code clearer in >representing the current use of the object, and catch errors earlier. > >I believe it could also simplify the global typing problem, since >read-only variables would already be limited as to the range of >features available to them, but I haven't had time to consider this >in any depth. Have any of the theorists out there done any work >along these lines? Yes. I have been thinking along precisely these lines and have come up with some ideas which I call "Fine Grain Inheritance". Below is a short summary. I have a couple of papers submitted to `Software -- Practice and Experience' and ECOOP '91. (No offence to the TOOLS '91 people, just a matter of timing). I believe you should split your class up into lots and lots of little superclasses, each implementing one elementary concept (such as "first item" or "read/write current item") and usually having only one or two attributes. Then go through your class heirarchy and split all classes up into "read-only" and "write-only" varients. Then implement them. Don't be frightened of the large amounts of repeated inheritance you wind up with, its OK. Most of your classes will be deferred and you should make all attributes into deferred functions as well, hence allowing them to be either functions or variables in descendant classes. Hence in Dr. Meyer's example, POLYGON will have (amongst others) an ancestor LIST[ VERTEX ], which will have ancestors LIST_READ[ VERTEX ] and LIST_WRITE[ VERTEX ]. RECTANGLE will have (amongst others) an ancestor LIST_READ[ VERTEX_READ ]. Hence not only is it impossible to add or remove verteces of RECTANGLE, it is also impossible to move a vertex (short of reverse assignment). On the other hand a function which needs to enumerate the verteces of a shape will be able to handle both POLYGON and RECTANGLE by taking an argument of type LIST_READ[ VERTEX_READ ]. This scheme becomes completely flexible when a new type rule (which I call the "Sibling-Supertype" rule) is added which (put briefly) allows the following. If the supertypes of A are a subset of the supertypes of B, then A is a supertype of B. These ideas allow the development of totally flexible class libraries. The only problem comes when coarse grain libraries must be reused as well. Of course, tools like "flat" and "short" become absolutely vital in fine grain libraries, as does multiple inheritance. See you all at TOOLS '91. Paul. -- Paul Johnson UUCP: <world>!mcvax!ukc!gec-mrc!paj --------------------------------!-------------------------|------------------- GEC-Marconi Research is not | Telex: 995016 GECRES G | Tel: +44 245 73331 responsible for my opinions. | Inet: paj@uk.co.gec-mrc | Fax: +44 245 75244
chip@tct.uucp (Chip Salzenberg) (02/05/91)
According to sakkinen@jytko.jyu.fi (Markku Sakkinen): >In article <27A86309.281A@tct.uucp> chip@tct.uucp (Chip Salzenberg) writes: >>RECTANGLE::add_vertex(): a do-nothing function body that returns a >>failure indication (if appropriate). > >This is only possible if either: >1. 'add_vertex' was originally declared with a return value or parameter > that indicates success or failure. >2. The language has an exception mechanism ... I discern that my suggestion was inadequate. I withdraw it. I now agree, however, with those who suggest that RECTANGLE is not an appropriate derivation of POLYGON. There is an assumption built into a POLYGON interface that includes an add_vertex() function: that a POLYGON may be modified in its number of vertices. The derivation of RECTANGLE from POLYGON violates that assumption. Separate inheritance subtrees of POLYGON, such as FIXED_POLYGON and VARIABLE_POLYGON, now seem to me the best way to go. -- Chip Salzenberg at Teltronics/TCT <chip@tct.uucp>, <uunet!pdn!tct!chip> "Most of my code is written by myself. That is why so little gets done." -- Herman "HLLs will never fly" Rubin
pfkeb@ebnextk.SLAC.Stanford.EDU (Paul Kunz) (02/05/91)
In article <27ADD06B.46A3@tct.uucp> chip@tct.uucp (Chip Salzenberg) writes:
I now agree, however, with those who suggest that RECTANGLE is not an
appropriate derivation of POLYGON.
There is an assumption built into a POLYGON interface that includes an
add_vertex() function: that a POLYGON may be modified in its number of
vertices. The derivation of RECTANGLE from POLYGON violates that
assumption. Separate inheritance subtrees of POLYGON, such as
FIXED_POLYGON and VARIABLE_POLYGON, now seem to me the best way to go.
--
Chip Salzenberg at Teltronics/TCT <chip@tct.uucp>, <uunet!pdn!tct!chip>
"Most of my code is written by myself. That is why so little gets done."
-- Herman "HLLs will never fly" Rubin
I've been following this thread a few days but have not had time to
respond but have come to the same conclusion. If POLYGON is going to
be SuperClass of RECTANGLE (and TRIANGLE and SQUARE) then it can not
possibly have a add_vertex() function. This is the error. Only
a very specialized class of VARIABLE_POLYGON which inherits from POLYGON
can have such a method.
In my mind the confusion comes from the fact that a RECTANGLE (or
TRIANGLE or SQUARE) is something real that we can see, feel, and touch.
They all inherit from something generic, not real, not touchable. On the
other hand, a n-point POLYGON is also real that can be changed to
n+1 points. It inherits from something generic as well. The mistake
is to call this generic thingy a POLYGON and put a add_vertex function
in it.
That's my two cents.
caseau@maya.bellcore.com (Yves J Caseau) (02/05/91)
Here is my $0.02 contribution to the problem: ============================================= 1. This is a well-known and common problem. In the type theory and object-oriented database community, the example is a class point with an instance variable x, and the subclass is the set of point for which x is between 0 and 10. So if we change the instance variable, the "class" to which the object belongs must change, or an error must be detected. The question is: given a class A, and a subclass B which has a characteristic property, how to represent it into an object-oriented system? Obviously it depends if your system supports such characteristic properties. If it does (Eiffel with invariants, Semantic networks with specialisation predicates (eg CLASSIC), LORE with selection sets, FOL-based object-oriented systems) you have a "nice" solution: inheritance with automatic error detection (Eiffel: the invariant is violated) or reclassification. If the system does not support such properties, you have to implement them yourself, copying one of the two previous solutions. This means that each method that returns an object from B must be specialized (if it is inherited) so as to check the characteristic property (and decide between error or re-classification, according to the language features (C++ vs. CLOS) 2. My solution to the POLYGON/RECTANGLE problem. I tend to be simple minded: a. a RECTANGLE is a POLYGON (thefore there must be inheritance) b. Adding a vertex to a polygon produces ANOTHER polygon (a different mathematical object) c. Thus, rectangle inherit the property add_vertex, which produces a pentagone from a rectangle Don't flame me on this, I know this is cheating ... but it makes a lot of sense.
rmartin@clear.com (Bob Martin) (02/05/91)
In article <27A86309.281A@tct.uucp> chip@tct.uucp (Chip Salzenberg) writes: >According to bertrand@eiffel.UUCP (Bertrand Meyer): >>A class POLYGON exists and has a procedure >>``add_vertex'', which it exports. You want >>to add a class RECTANGLE. What do you do? >> >>1. Decide not to use inheritance. >>2. Redo the existing inheritance hierarchy. >>3. Accept that RECTANGLE inherits from POLYGON and does not export >> ``add_vertex''. ... and disallow polymorphic assignments of the form >> p := r where p is of type POLYGON and r of type RECTANGLE. >>4. Accept that RECTANGLE inherits from POLYGON and does not export >> ``add_vertex''. Permit polymorphic assignments of the above form; >> but have the type checker reject as invalid any system which would >> contain both such an assignment and a call p.add_vertex (...), since >> it may lead to a run-time inconsistency. > >In C++, the add_vertex() member function would almost certainly be >virtual; [...] add_vertex becomes a non-problem. The >implementor of RECTANGLE simply provides an appropriate definition of >the RECTANGLE::add_vertex(): a do-nothing function body that returns a >failure indication (if appropriate). > >In the real world, how is this solution deficient? Assume that this example were part of a very large system which added points to POLYGONs all over the place. Assume also that there had never before been a reason for add_point to fail. Your proposed solution _changes_ the interface to class POLYGON since now some POLYGONs (or their derivatives i.e. RECTANGLEs) will issue failure for add_point. Thus it breaks old working code. > >>As an aside (meant to be thought- rather than flame-provoking) >>I have come to realize, although not early enough, that >>information hiding is a greatly overrated goal. This is not a flame, it is a provoked thought. Some people may overrate a goal. That does not devalue the goal. > -- +-Robert C. Martin-----+:RRR:::CCC:M:::::M:| Nobody is responsible for | | rmartin@clear.com |:R::R:C::::M:M:M:M:| my words but me. I want | | uunet!clrcom!rmartin |:RRR::C::::M::M::M:| all the credit, and all | +----------------------+:R::R::CCC:M:::::M:| the blame. So there. |
adam@visix.com (Adam Kao) (02/06/91)
I have some random thoughts on this topic; Yves J Caseau's posting brought my ideas together, to the point where I would like to pursue them further in public. I have always felt uncomfortable with the emphasis on "Class" in languages like Smalltalk, C++ etc. To me, the notion of an Object seems much more basic and fundamental than a Class. Everybody knows that real-world classes are not well-behaved; they are not disjoint, they are not proper subsets, class membership changes over time. I see object oriented techniques such as tree heirarchies, multiple inheritance, etc., as attempts to make Classes well-behaved. But in doing so, I feel that we increasingly distance our notion of Class from the common-sense notion of class, which necessarily makes object-oriented programming harder to understand. In the real world, objects do not, strictly speaking, belong to a class. A classification system is imposed on real objects by our world view; class membership is an answer to a question we phrase. Different question, different answer. To my mind, classes can be viewed as schema that define invariants among their members. Every object is then potentially a member of as many classes as we care to define. Since any trait of an object can be considered a defining invariant for some class, it then becomes inevitable that any trait change will remove the object from some classes and add it to others. An abstract RECTANGLE satisfies all the invariants of a POLYGON, so it is natural to have RECTANGLE inherit from POLYGON. But a RECTANGLE has additional invariants, so operations that are closed over POLYGONs are not closed over RECTANGLEs. We know how to add vertices to POLYGONs. I think it ridiculous to say we do not know how to add vertices to RECTANGLEs. The problem is, adding a vertex to a RECTANGLE gives us something that is not a RECTANGLE. I do not see this as a problem. Why do we expect that all operations on an Object must be closed over its Class? Most languages have type-casting and type-promotion. In C, we can add a double to an int, and the result will be a double. Is this kind of arithmetic forbidden in object-oriented programming? To me, the statements "p := r" and "p.add_vertex(...)" are perfectly compatible. The "p := r" should imply a cast; if we want to make the assignment, it means we want to treat rectangles as polygons. Surely the explicit conversion "p := r.make_polygon()" followed by "p.add_vertex(...)" is acceptable; why not accept even "r.add_vertex(...)" as a shorthand? I believe that this discussion is evidence that our notion of a Class is too weak to support all the uses we expect. Of course, I was predisposed to this conclusion. But, I most certainly hope to hear more discussion on any side of this issue. Adam
blerner@empire.cs.umass.edu (Barbara Lerner) (02/06/91)
> From: adam@visix.com (Adam Kao) > [..] > An abstract RECTANGLE satisfies all the invariants of a POLYGON, so it > is natural to have RECTANGLE inherit from POLYGON. But a RECTANGLE > has additional invariants, so operations that are closed over POLYGONs > are not closed over RECTANGLEs. > > We know how to add vertices to POLYGONs. I think it ridiculous to say > we do not know how to add vertices to RECTANGLEs. The problem is, > adding a vertex to a RECTANGLE gives us something that is not a > RECTANGLE. > > I do not see this as a problem. Why do we expect that all operations > on an Object must be closed over its Class? > > Most languages have type-casting and type-promotion. In C, we can add > a double to an int, and the result will be a double. Is this kind of > arithmetic forbidden in object-oriented programming? > > To me, the statements "p := r" and "p.add_vertex(...)" are perfectly > compatible. The "p := r" should imply a cast; if we want to make the > assignment, it means we want to treat rectangles as polygons. Surely > the explicit conversion "p := r.make_polygon()" followed by > "p.add_vertex(...)" is acceptable; why not accept even > "r.add_vertex(...)" as a shorthand? [..] The distinction between adding a vertex to a polygon and adding a vertex to a rectangle is the following. When a vertex is added to a polygon, no new object is created. The same polygon object exists but has one more vertex. If we take the point of view that we can add a vertex to a rectangle with the result begin a pentagon, we now have two choices: 1. Create a new Pentagon object that is a "copy" of the rectangle with the added vertex. 2. Add the vertex to the rectangle object, and change its class from rectangle to pentagon. The first solution is fundamentally different from adding a vertex to the object. For example, if we have an object O with a field p of type polygon. O.p := make_polygon(..) O.p.add_vertex(..); In this case, O.p will be a polygon with one more vertex than it was originally created with. Suppose instead, we do: O.p := make_rectangle(..) O.p.add_vertex(..); Now, O.p is still a rectangle since we haven't changed O.p, but created a new object instead. Of course, we could say: O.p := O.p.add_vertex(..); This places the burden on the user of add_vertex to make sure that all references to the rectangle are appropriately updated. Keep in mind that there may be more than one object referenceing the rectangle. The second solution is not type safe in the following situation. Suppose we have an object O with two fields: p of type polygon, and r of type rectangle. Now suppose we execute the following code with add_vertex changing the class of the rectangle to pentagon. O.r := make_rectangle(..); O.p := O.r; O.p.add_vertex(..); If we change the class of the rectangle to pentagon, then O.r now refers to a pentagon while the class of O.r is declared to be rectangle. *Accurately* detecting this situation at compile-time is probably impossible. Detecting it at runtime would require every object to maintain backpointers to all objects that reference it, so that all class changes could be type-checked. Barbara Lerner -- Barbara Staudt Lerner Deparment of Computer and Information Science Lederle Graduate Research Center University of Massachusetts Amherst, MA 01003 blerner@cs.umass.edu
bertrand@eiffel.UUCP (Bertrand Meyer) (02/07/91)
From <1991Feb5.130359.9735@bellcore.bellcore.com> by caseau@maya.bellcore.com: > 1. This is a well-known and common problem. In the type theory and object-oriented > database community, the example is a class point with an instance variable x, > and the subclass is the set of point for which x is between 0 and 10. So if we > change the instance variable, the "class" to which the object belongs must > change, or an error must be detected. > It may be a well-known and common problem but I don't remember reading anything really enlightening in the literature about the fields mentioned by Yves Caseau. Here is a way to model Mr. Caseau's example. There is a class A class A export x, set_x, ... feature x: INTEGER; set_x (new_value: INTEGER) is do x := new_value end invariant -- Nothing relevant end and an heir B: class B export x, ... feature ... invariant 0 <= x; x <= 10 end B is fine since its invariant is stronger than the invariant of the parent class A. There is a a problem with procedure set_x, however: the basic rule of class correctness states (in particular) that every exported routine should preserve the invariant. (See chapter 7 of OOSC and the article ``Programming as Contracting''.) Since set_x does not necessarily preserve the invariant, ergo - it cannot be exported in B. So we are back to the situation discussed in previous postings, and the need to allow a class to hide features exported by its parents. It is not possible to redefine set_x in B to be more demanding, as in class B export x, ... feature ... set_x (new_value: INTEGER) is require 0 <= new_value; new_value <= 10 do x := new_value end invariant 0 <= x; x <= 10 end This does not work, since the new precondition would be stronger (again, see above references). It would make it impossible for a client to guarantee that a call will be correct through a scheme of the form [X] b1: B; ... if some_consistency_check (my_value) then b1.set_x (my_value) else ... Other action ... end There is a conceptual trick, however, that often works. Make the precondition abstract by introducing an exported function acceptable_value in A: acceptable_value (n: INTEGER): BOOLEAN is do Result := true end and redefine it in B (keeping it exported): acceptable_value (n: INTEGER): BOOLEAN is do Results := (0 <= x) and (x <= 10) end Then you can indeed export set_x in B because clients can use scheme [X] safely under the following variant: [Y] if acceptable_value (my_value) then b1.set_x (my_value) else ... Other action ... end The precondition of set_x does not change in B. (Of course the concrete precondition changes since B redefines function acceptable_value; and it may even appear to have been strengthened, violating the contracting rules as before. But this is in fact not true. What counts is the abstract precondition, as seen from the oustside, by clients; and that one has remained the same, as expressed by the property lambda n . acceptable_value (n).) This technique is used quite frequently in the Basic Eiffel Library. -- Bertrand Meyer bertrand@eiffel.com
bertrand@eiffel.UUCP (Bertrand Meyer) (02/07/91)
From <1991Feb5.130359.9735@bellcore.bellcore.com> by caseau@maya.bellcore.com (Yves J Caseau): > 2. My solution to the POLYGON/RECTANGLE problem. > > I tend to be simple minded: > a. a RECTANGLE is a POLYGON (thefore there must be inheritance) > b. Adding a vertex to a polygon produces ANOTHER polygon (a different > mathematical object) > c. Thus, rectangle inherit the property add_vertex, which produces > a pentagone from a rectangle > > Don't flame me on this, I know this is cheating ... but it makes a lot of > sense. If one transposes the problem to applicative (rather than imperative) programming, where there are no procedures but only functions which do not produce side-effects, and also removes reference assignment (which introduces aliasing), this solution indeed works. Mirandeiffel? -- -- Bertrand Meyer bertrand@eiffel.com
rick@tetrauk.UUCP (Rick Jones) (02/07/91)
In article <27A9AB0C.4794@tct.uucp> chip@tct.uucp (Chip Salzenberg) writes: >According to rick@tetrauk.UUCP (Rick Jones): >>Given that this thread is starting to look like an Eiffel v C++ debate, there >>is a significant aspect of Eiffel which has been overlooked - assertions. > >Please don't cloud discussions with irrelevant issues. > >Inheritance and information hiding issues are orthagonal to assertion >issues. A given language may support the former, the latter, both, or >neither. Let's keep the discussions clear. A significant theme of this thread was the postulation by some that the notion of "private" was essential in order to prevent subclasses from breaking their parent's invariants. The connection between assertion and information hiding had already been made. My point is that, as far as maintaining invariants is concerned, there is more than one way to skin a cat. In fact my view is that assertions are a superior method. If "private" is used to achieve this, then information hiding and assertions are not being kept separate. If assertions are used the two issues are separated. In summary, I agree with Chip on the orthogonality, but the issue is neither irrelevant nor, it seems, entirely free of clouds. A little discussion might let in some light. -- Rick Jones Tetra Ltd. Maidenhead, Was it something important? Maybe not Berks, UK What was it you wanted? Tell me again I forgot rick@tetrauk.uucp -- Bob Dylan
schwartz@karl.cs.psu.edu (Scott Schwartz) (02/07/91)
Bertrand Meyer writes:
Mirandeiffel?
Hmmm. Maybe ML can do this, using functors. Any ML wizards reading this?
--
Scott Schwartz schwartz@cs.psu.edu
adam@visix.com (Adam Kao) (02/07/91)
Barbara Lerner writes: > The distinction between adding a vertex to a polygon and adding a > vertex to a rectangle is the following. When a vertex is added to a > polygon, no new object is created. The same polygon object exists but > has one more vertex. > If we take the point of view that we can add a vertex to a rectangle > with the result begin a pentagon, we now have two choices: > 1. Create a new Pentagon object that is a "copy" of the rectangle > with the added vertex. > 2. Add the vertex to the rectangle object, and change its class > from rectangle to pentagon. Hmm, I guess I did gloss over this distinction. I do understand the uses and problems of sharing mutable objects. Choice (1) defines nonshared or immutable use, choice (2) defines mutable use. I see these choices as complementary. Our problem with (2) is that someone else can modify the shared object in ways which I consider illegal. All users of the object should agree on the set of allowed changes. A. A Class must support all operations of its Parent Class. B. A Class must enforce all invariants that users wish to preserve on member Objects. (Allowing (2) makes Class RECTANGLE differ from our specific abstract rectangle class in the sense defined by (B). Class membership in the programming sense is actually only one of many possible invariants that we might wish to maintain to define an abstract class.) These goals conflict, which leads me to say that our notion of Class cannot support all the uses we expect. (A) and (B) are different questions; they lead to different classification schemes. If we choose to preserve (A), then Siblings will be able to change into each other. If we choose to preserve (B), then Classes specialize their Parents by reducing the allowed transformations. Any solution that tries to define a singular Class concept satisfying both (A) and (B) will be non-intuitive, at best. Adam > Barbara Staudt Lerner > Deparment of Computer and Information Science > Lederle Graduate Research Center > University of Massachusetts > Amherst, MA 01003 > blerner@cs.umass.edu PS I notice you edited the followup line to comp.lang.eiffel. Unfortunately, I know almost nothing about eiffel. I would rather continue this discussion in comp.object, or, if you feel it is more appropriate, via email.
sakkinen@tukki.jyu.fi (Markku Sakkinen) (02/07/91)
In article <1991Feb4.223823.3198@runx.oz.au> chrisv@runx.oz.au (Chris Velevitch) writes: > ... >Yes I agree that abstraction is the real goal of OOD. I get the feeling >exporting features is a smoke screen. Would it not be better to simply make >all features of a class implicitly public (as in Smalltalk)? This would free >the designer from having to think about what features would be useful to >clients and help concentrate on what the interface should be in the context >of the problem. I agree with the designers of the majority of newer OOPL's that the Smalltalk way is worse. It's exactly the question about the interface. Following Ole-Johan Dahl, the grand old man of OOP, think of objects or classes offering an interface of procedures (methods) to potential clients, as an analogy of procedures (in a subroutine library, say) offering an interface of formal parameters to potential invokers. The definitions of formal parameters are important to the invokers of a procedure, but most procedures also contain local variables that are none of the invokers' business. Likewise, the definitions of public operations are important to the clients of a class, but some private operations may well be useful to implement the public ones. In most cases they could be eliminated by code duplication (contrary to the code reuse goal of OOP), but that is not possible if a private procedure is recursive. If the class designer later notes that some originally private feature should be offered to clients, he can simply change it from private to public. From the clients' viewpoint it is exactly equivalent with adding a brand-new feature to the class. Our procedure analogy does not work very well here any longer, but in some languages the formal parameter list of a procedure can be extended with optional parameters without disrupting existing code. I support the "orthogonal" view to visibility that is taken e.g. by current Simula and C++, that even instance variables can be declared public or private (or "protected"). Of course, declaring a public instance variable in a class is a strong commitment that restricts the freedom in the implementation and evolution of the class, but a class designer should have the possibility to such a commitment. _Any_ public feature constitutes a commitment and restriction, especially if its declaration promises something about its semantics (like the assertions of Eiffel) instead of containing only the signature. Markku Sakkinen Department of Computer Science and Information Systems University of Jyvaskyla (a's with umlauts) PL 35 SF-40351 Jyvaskyla (umlauts again) Finland SAKKINEN@FINJYU.bitnet (alternative network address)
richieb@bony1.bony.com (Richard Bielak) (02/07/91)
In article <1991Feb6.045542.791@visix.com> adam@visix.com (Adam Kao) writes: > [...lots of stuff deleted...] >We know how to add vertices to POLYGONs. I think it ridiculous to say >we do not know how to add vertices to RECTANGLEs. The problem is, >adding a vertex to a RECTANGLE gives us something that is not a >RECTANGLE. > >I do not see this as a problem. Why do we expect that all operations >on an Object must be closed over its Class? > Perhaps the "add_vertex" routine should be a function? How about this: add_vertex : POLYGON is do -- whatever end; Then if "r" is a RECTANGLE, we can call "r.add_vertex" and get back an object that is not a RECTANGLE. ...richie -- +----------------------------------------------------------------------------+ | Richie Bielak (212)-815-3072 | "The sights one sees at times makes one | | Internet: richieb@bony.com | wonder if God ever really meant for | | Bang: uunet!bony1!richieb | man to fly." -- Charles Lindbergh |
craig@Neon.Stanford.EDU (Craig D. Chambers) (02/08/91)
In article <BLERNER.91Feb6100509@empire.cs.umass.edu> blerner@empire.cs.umass.edu (Barbara Lerner) writes: > > [stuff deleted] > >If we take the point of view that we can add a vertex to a rectangle >with the result begin a pentagon, we now have two choices: > > [stuff deleted] > > 2. Add the vertex to the rectangle object, and change its class > from rectangle to pentagon. > > [stuff deleted] > >The second solution is not type safe in the following situation. >Suppose we have an object O with two fields: p of type polygon, and r >of type rectangle. Now suppose we execute the following code with >add_vertex changing the class of the rectangle to pentagon. > > O.r := make_rectangle(..); > O.p := O.r; > O.p.add_vertex(..); > >If we change the class of the rectangle to pentagon, then O.r now >refers to a pentagon while the class of O.r is declared to be >rectangle. If you take the view that rectangle is a specialized *implementation* of polygons, but the *type* of rectangle is still polygon, then this class-changing operation is perfectly type-safe (as I mentioned in an earlier posting). The add_vertex operation on rectangles modifies the rectangle *in place* to be a pentagon-represented polygon; the interface to the rectangle remains unchanged. This facility requires that some classes be marked as not defining new types but instead new implementations of existing types. I think that enabling the programmer to change the implementation of objects at run-time is a very powerful feature, and programmers will find new and useful ways of applying this power as they become used to thinking about an object's representation and implementation as mutable. This operation is rather complicated to implement efficiently and describe coherently in a class-based language, but is quite straightforward in a prototype-based language supporting dynamic object inheritance (delegation) instead of static class inheritance. -- Craig Chambers P.S. I'm reposting this to comp.object from comp.lang.eiffel; the message I replied to had unexpectedly edited the followup-to line. Please keep this discussion in both newsgroups (or at least in comp.object).
om@icsib29 (Stephen M. Omohundro) (02/08/91)
Just a quick note on the Sather solution to the POLYGON vs. RECTANGLE problem. In Sather the distinction is made between a variable declared as "pa:POLYGON" which is guaranteed to hold an actual polygon object and "pd:$POLYGON" which can hold a polygon or any of its descendents. In Sather there is no neccessity for descendent classes to define all the features of its ancestors (and there is an "UNDEFINE" mechanism to eliminate features). If the call "pd.add_vertex" appears, then the compiler checks all descendents of POLYGON and complains if "add_vertex" is missing from any of them or has an incompatible interface. On the other hand, the call "pa.add_vertex" is perfectly legal regardless of the descendent's class definitions. If "add_vertex" is only meant to apply to actual POLYGON objects, then such declarations guarantee that they will not be applied to objects of descendent classes. In this way the use of inheritance to reuse features of a parent class in a child class is somewhat separated from its use in constraining dynamic dispatch. --
jgk@osc.COM (Joe Keane) (02/08/91)
One possibility is that RECTANGLE and POLYGON are separate concepts which have nothing to do with each other. Then there's not much to talk about. Let's assume that the RECTANGLE class is entirely an optimization. It's simply a way of doing POLYGON faster in those cases where you can. This is quite likely in a graphics application. You can do a lot of things more efficiently if you know something is a rectangle rather than a general polygon. For example, the X protocol has special cases for rectangles. Some routines will know about RECTANGLE and deal with its instances differently. Those routines which act on polygons would test if the polygon is actually a RECTANGLE instance. There may be some interface which applies only to RECTANGLE instances. For example, we may have methods which return the corners of the rectangle, or the attributes may be directly accesible. However, most methods do not know about RECTANGLE, only POLYGON. Given this, we should insist that the RECTANGLE optimization be transparent. We should be able to add a vertex to any polygon, regardless of its implementation class. That's what polymorphism is all about. Saying that it works for accessors but not for modifiers is going only half way. If we add a vertex to a RECTANGLE instance, then it will have to change its implementation class to something else. Perhaps we have a PENTAGON class or maybe it's just converted back to a generic POLYGON. Many OO languages don't deal well with this, although CLOS does. If a variable is declared as POLYGON, we can add a vertex to it. It should have no idea that the implementation class is being shifted from RECTANGLE to PENTAGON under it. On the other hand, if a variable is declared as RECTANGLE, we can't add a vertex. This is a type error and hopefully will be caught at compile time. Global type checking doesn't make sense. Either an operation is correct or it isn't. How can the definition of a class depend on how you use it? I won't even talk about the practical problem of having your type checker hunt down everyone who uses a given class.
sakkinen@tukki.jyu.fi (Markku Sakkinen) (02/08/91)
In article <1991Feb7.151212.25200@bony1.bony.com> richieb@bony1.UUCP (Richard Bielak) writes: >In article <1991Feb6.045542.791@visix.com> adam@visix.com (Adam Kao) writes: >> >[...lots of stuff deleted...] > >>We know how to add vertices to POLYGONs. I think it ridiculous to say >>we do not know how to add vertices to RECTANGLEs. The problem is, >>adding a vertex to a RECTANGLE gives us something that is not a >>RECTANGLE. >> >>I do not see this as a problem. Why do we expect that all operations >>on an Object must be closed over its Class? >> > >Perhaps the "add_vertex" routine should be a function? How about >this: > > add_vertex : POLYGON is > do > -- whatever > end; > > >Then if "r" is a RECTANGLE, we can call "r.add_vertex" and get back >an object that is not a RECTANGLE. This looked at first like a very good solution to me. However, it fits best if the interfaces of Polygon and other related classes are even in general defined in a "functional" fashion: i.e. modifying operations do not modify the "self" object but return a new object. If some operations modify the object itself and others return a new object instead, the interface is very inconsistent. The functional approach has its severe drawbacks, too. Suppose that there are several references from other objects to this geometric object (initially a rectangle). If the intended semantics is that the sharing relationship between these references is maintained (which would be the typical case), one would need to have back pointers to all those other objects in order to make them refer to the modified polygon. Markku Sakkinen Department of Computer Science and Information Systems University of Jyvaskyla (a's with umlauts) PL 35 SF-40351 Jyvaskyla (umlauts again) Finland SAKKINEN@FINJYU.bitnet (alternative network address)
chip@tct.uucp (Chip Salzenberg) (02/08/91)
According to rick@tetrauk.UUCP (Rick Jones): >In article <27A9AB0C.4794@tct.uucp> chip@tct.uucp (Chip Salzenberg) writes: >>Inheritance and information hiding issues are orthagonal to assertion >>issues. A given language may support the former, the latter, both, or >>neither. Let's keep the discussions clear. > >The connection between assertion and information hiding >had already been made. Okay, I'll grant that point. My reaction was perhaps stronger than necessary because I don't want this discussion to degenerate into "my language can beat up your language." I'm sorry if I overdid it. >My point is that, as far as maintaining invariants is concerned, there is more >than one way to skin a cat. In fact my view is that assertions are a superior >method. I can see a place for both. There are times when strict invariant checking would have caught problems in my code. The GNU C++ compiler includes a "wrapper" mechanism, which replaces calls to functions with specific return and parameter types with calls to a wrapper function, one parameter of which is the address of the "real" function. This mechanism is too limited for general assertion checking, however, because of the requirement that each set of return and parameter types have its own wrapper. I read about a proposed extension to C++ called A++, or Annotated C++, by Marshall Cline of Clarkson University and Doug Lea of SUNY. It defines assertions for the class ("legal:") and for member functions ("axioms:" with "require" and "promise" assertions). The resemblance of A++'s assertions to those of Eiffel is acknowledged in the introduction to the paper, ``Using Annotated C++.'' (Similar problems lead to similar solutions.) They hoped to avoid redundant checks when member functions call each other, and described obvious code generation tricks to that end. I wonder if they ever got their proof-of-concept compiler working...
sakkinen@tukki.jyu.fi (Markku Sakkinen) (02/08/91)
[Sorry, again I failed to notice the follow-up redirection to 'comp.lang.eiffel', which does not make much sense to me when the posting is about Sather.] In article <10900@pasteur.Berkeley.EDU> om@icsib29 (Stephen M. Omohundro) writes: >Just a quick note on the Sather solution to the POLYGON vs. RECTANGLE problem. >In Sather the distinction is made between a variable declared as "pa:POLYGON" >which is guaranteed to hold an actual polygon object and "pd:$POLYGON" which can >hold a polygon or any of its descendents. In Sather there is no neccessity for >descendent classes to define all the features of its ancestors (and there is an >"UNDEFINE" mechanism to eliminate features). If the call "pd.add_vertex" >appears, then the compiler checks all descendents of POLYGON and complains if >"add_vertex" is missing from any of them or has an incompatible interface. > ... An obvious implication of the above principles would be that modifications of classes and methods in a Sather environment will often cause _massive_ recompilations, wouldn't it? In particular, if some new class leaves any inherited feature F undefined, then all clients of all appropriate superclasses must be rechecked. (If the programming environment registers client relationships to the utmost detail, those client classes that invoke F through a "heterogeneous" variable - such as 'pd' above - can be pinpointed directly.) Markku Sakkinen Department of Computer Science and Information Systems University of Jyvaskyla (a's with umlauts) PL 35 SF-40351 Jyvaskyla (umlauts again) Finland SAKKINEN@FINJYU.bitnet (alternative network address)
blerner@empire.cs.umass.edu (Barbara Lerner) (02/08/91)
In article <1991Feb7.205232.23548@Neon.Stanford.EDU> craig@Neon.Stanford.EDU (Craig D. Chambers) writes: > If you take the view that rectangle is a specialized *implementation* > of polygons, but the *type* of rectangle is still polygon, then this > class-changing operation is perfectly type-safe (as I mentioned in an > earlier posting). The add_vertex operation on rectangles modifies the > rectangle *in place* to be a pentagon-represented polygon; the > interface to the rectangle remains unchanged. This facility requires > that some classes be marked as not defining new types but instead new > implementations of existing types. This is an interesting solution. However, to clarify a point, it seems to me that this solution implies that clients of polygon do not know about the different implementations, but rather the system somehow chooses the correct implementation, probably based on constraints defined by the implementation. If this is true, then clients of polygon can only declare variables to be of type polygon, and not of type rectangle. (In fact, I think it would be a mistake to call these alternative implementations "classes".) Multiple implementations support the definition of algorithms of varying efficiency, but offer no client control over which implementation is chosen. (There may be faster algorithms for drawing rectangles than other polygons, but the client can't say they only want to consider rectangles.) Subtyping offers the client finer-grained control over the desired semantics, but (in strong type-checking languages) restricts the programmer's flexibility in defining subtypes. (The client can restrict a variable to be a rectangle, but now the programmer can't define add_vertex on polygons.) I suspect there are uses for both solutions. -- Barbara Staudt Lerner Deparment of Computer and Information Science Lederle Graduate Research Center University of Massachusetts Amherst, MA 01003 blerner@cs.umass.edu
dbc@cimage.com (David Caswell) (02/08/91)
.>We know how to add vertices to POLYGONs. I think it ridiculous to say .>we do not know how to add vertices to RECTANGLEs. The problem is, .>adding a vertex to a RECTANGLE gives us something that is not a .>RECTANGLE. .> I don't think that we know how to add vertices to all POLYGONs. Rectangles for example. I think the solution is to put the rectangle methods on polygon and have them assert that the number of vertices equals 4. They could be put in another file or whatever if you're worried about readability. This problem comes up in countless situations. Unfortunately there is not a perfect solution.
wright@hsi86.hsi.com (Gary Wright) (02/09/91)
In article <27A9AB0C.4794@tct.uucp> chip@tct.uucp (Chip Salzenberg) writes: >Inheritance and information hiding issues are orthagonal to assertion >issues. A given language may support the former, the latter, both, or >neither. Let's keep the discussions clear. At least in Eiffel, the semantics of inheritance are not orthagonal to assertions. An interesting part of this discussion has been what role assertions (pre-conditions, post-conditions, invariants) have in an inheritance model and how does that restrict the way a class can be changed with respect to its parent(s). I'd like to hear more, not less, regarding this issue. -- Gary Wright ...!uunet!hsi!wright 3M Health Information Systems wright@hsi.com
cjmchale@cs.tcd.ie (Ciaran McHale) (02/09/91)
Here's my $0.02 worth on the topic concerning rectangles and
polygons.
Some people have pointed out that since rectangle doesn't
support all the operations which might be performed on a
polygon, rectangle should *not* be a subtype of polygon.
(Other approaches were also suggested.) Instead, the graphics
hierarchy should look like:
graphic (base class)
/\
/ \
/ \
rectangle polygon
A problem with this approach is that rectangle is certainly
similar to polygon in many respects and so we would like to
have it derived from polygon so that we can reuse code.
Bascially we're torn between two seemingly conflicting issues:
1. We can't have rectangle as a subtype of polygon
since add_vertex doesn't make sense when applied
to a polygon.
2. We do not want to duplicate code which is common
between rectangle and polygon.
One way to solve this dilemma is for the language to have two
hierarchies rather than just one. There would be a TYPE
hierarchy and an IMPLEMENTATION (CODE) hierarchy. Using this
scheme, rectangle and polygon can be siblings in the TYPE
hierarchy, but rectangle could (if the programmer wanted to)
inherit code from polygon in the IMPLEMENTATION heierarchy.
TYPE HIERARCHY IMPLEMENTATION HIERARCHY
-------------- ------------------------
graphic graphic
/\ |
/ \ |
/ \ polygon
rectangle polygon |
|
rectangle
Note that the IMPLEMENTATION hierarchy does not have to
mirror the TYPE hierarchy. The IMPLEMENTATION of rectangle
will include the add_vertex operation (inherited from the
IMPLEMENTATION of polygon) but this will be "private" since
it is not specified in rectangle's TYPE.
Ciaran.
--
Ciaran McHale "Verbosity says it all" ____
Department of Computer Science, Trinity College, Dublin 2, Ireland. \ /
Telephone: +353-1-772941 ext 1538 FAX: +353-1-772204 \/
Telex: 93782 TCD EI email: cjmchale@cs.tcd.ie
mjl@cs.rit.edu (Michael J Lutz) (02/10/91)
In article <1991Feb8.171341.19413@cs.tcd.ie>, cjmchale@cs.tcd.ie (Ciaran McHale) writes: |>Bascially we're torn between two seemingly conflicting issues: |> |> 1. We can't have rectangle as a subtype of polygon |> since add_vertex doesn't make sense when applied |> to a polygon. |> 2. We do not want to duplicate code which is common |> between rectangle and polygon. |> |>One way to solve this dilemma is for the language to have two |>hierarchies rather than just one. There would be a TYPE |>hierarchy and an IMPLEMENTATION (CODE) hierarchy. Note that this is the solution employed in POOL (Parallel Object Oriented Language), where the CLASS hierarchy is used to define implementation reuse, and the TYPE hierarchy is used to refine behavior. If a particular CLASS exhibits the behavior required of a TYPE, then objects in that class can be associated with variables of the TYPE. Check out the paper by America and van der Poel in the 1990 OOPSLA/ECOOP proceedings. --------- Mike Lutz Rochester Institute of Technology Rochester, NY 14623-0887 mjl@cs.rit.edu
bertrand@eiffel.UUCP (Bertrand Meyer) (02/10/91)
** Note: As Markku Sakkinen requested before, it would be nice if everyone left the original groups in the Newsgroups line. Someone, for some reason, had removed comp.lang.eiffel. I have put it back. ** end of note. This is simply to point out that the issue of ``implementation reuse'' versus ``specification reuse'' has nothing to do with this whole discussion. The debate (e.g. my original questions regarding polygons and rectangles) can be entirely confined, if need be, to the purely mathematical notion of abstract data types. We can talk about inheritance relations as being morphisms in categories, and we don't even need to refer to computers. I should add that ``implementation reuse'' is, for me, a term devoid both of meaning and of interest. The separation between concept and implementation in software is an historical evil, not a desirable situation. With a good notation we can remove the difference. For one thing, this is clearly the goal of Eiffel. But of course the human tendency to make things difficult when they should be simple is eternal. I also know that there is very little chance that anyone beyond Eiffel users will accept this today. It will take several years; too many people have been taught that implementation is dirty and ``specification'' or ``design'' is good. (See the lengthy letter in the January Communications of the ACM, by a gentleman from the US Coast Guard, claiming that he represents the trenches of the computing profession and, among other things, that software engineering has nothing to do with implementation.) Of course if one's world is an environment where implementation is done in Cobol or C, the need for something at a higher level is obvious. But this is irrelevant to object-oriented technology, or software engineering for that matter. Somewhere in his essays on linguistics, Roman Jakobson illustrates a point by telling the following story. A missionary was blaming natives because they always went everywhere naked. ``Cover your body!'', he would scream. Once they asked back: ``But you too have an area of uncovered flesh, right there, haven't you?''. ``Of course'', said the Father, ``but that's different! It's my face''. ``Well then'', retorted the natives, ``with us, everything is the face''. I like to think of this as a guide for software construction. One day we will stop being ashamed of implementation, not because we will have removed the need for implementation but for exactly the opposite reason: with us, everything is the implementation. -- Bertrand Meyer bertrand@eiffel.uucp
cjmchale@cs.tcd.ie (Ciaran McHale) (02/11/91)
This discussion has used a concrete example (involving polygons, rectangles and an add_vertex operation) to discuss an issue. From the top of my head I can recall the following approaches being suggested: o Having the add_vertex operation in polygon makes that class too specialised to be used as a superclass for rectangle. Reorganise the class hierarchy a bit to solve the problem. o The problem can be solved if the language allows an object to change its type at run-time. Thus applying add_vertex to a rectangle changes that object into a (pentagon or a) polygon. o Rectangle.add_vertex should be a null operation. o Rectangle.add_vertex should raise an exception. o Have very fine grain multiple inheritance. o Have a read_only interface to each class. The read_only interface for polygon would exclude add_vertex, and this resultant class is then suitable as a superclass for rectangle. (I'm sure that other approaches were discussed too.) Then I add in my $0.02 worth suggesting: o If a language has separate type and implementation hierarchies then rectangle can reuse code from polygon without having to support add_vertex in its type. Bertrand Meyer (article <498@eiffel.UUCP>) replies with: >This is simply to point out that the issue of ``implementation >reuse'' versus ``specification reuse'' has nothing to do >with this whole discussion. Why not? It _is_ an approach to solving the problem at hand. It may not be the best approach but I don't see why you are so eager to dismiss it out of hand. >The separation between concept and implementation in software is >an historical evil, not a desirable situation. With a good notation we >can remove the difference. >[...] >One day we will stop being ashamed of implementation, >not because we will have removed the need for implementation >but for exactly the opposite reason: with us, everything >is the implementation. I don't know how (in)correct you are. Only time will tell. But just because you feel that way does not mean that others share your beliefs. So until separation-between-type-and-implementation is widely accepted to be a bad idea, why not consider it where it might be useful? Ciaran. -- Ciaran McHale "Verbosity says it all" ____ Department of Computer Science, Trinity College, Dublin 2, Ireland. \ / Telephone: +353-1-772941 ext 1538 FAX: +353-1-772204 \/ Telex: 93782 TCD EI email: cjmchale@cs.tcd.ie
weigand@kub.nl (Hans Weigand) (02/11/91)
In article <494@eiffel.UUCP> bertrand@eiffel.UUCP (Bertrand Meyer) writes: >... >This does not work, since the new precondition would be stronger (again, >see above references). It would make it impossible for a client Perhaps the problem discussed here hinges on an inappropriate definition of the precondition. Usually, preconditions are thought to be *nessecary* conditions, and, moreover, the implicit assumption is that when all necessary conditions have been specified, these together are also sufficient. In this conception, preconditions can be inherited and strenghtened for subtypes. However, if a precondition is assumed to be sufficient, as Eiffel does, rather than necessary, then the inheritance runs counter to the usual line of specification. That is, the programmer first specifies the supertype, and specifies the preconditions that he deems necessary. However, when he later comes to subtypes, he typically wants to add more specific preconditions. In the "sufficiency" approach, the programmer should be aware of all subtypes details when he is designing the supertype, which is not reasonable. Perhaps you say that the "sufficiency" approach follows from the contract view, but I doubt. When I make a contract for X, and state my conditions (the obligations of the client), I do not commit myself more than needed. If the client wants to use my product Y too (the subtype), then I would like to be able to negotiate new conditions. These conditions then add to the first ones. Reference: Wieringa R, H Weigand, JJ Meyer, F Dignum: The Inheritance of Dynamic and Deontic Integrity Constraints. Annals of Mathematics and Artificial Intelligence, Nov 1990. -- Hans Weigand Tilburg University
paj@mrcu (Paul Johnson) (02/11/91)
In article <1991Feb7.205232.23548@Neon.Stanford.EDU> craig@Neon.Stanford.EDU (Craig D. Chambers) writes: >In article <BLERNER.91Feb6100509@empire.cs.umass.edu> blerner@empire.cs.umass.edu (Barbara Lerner) writes: > [Good stuff about the polygon-rectangle problem deleted. Briefly, > the idea was that rectangle.add_vertex turns the object into a > polygon] In passing, its worth noting that the InterViews solution to this very problem was similar to what was proposed in the referenced articles. If you asked for a rectangle, you got a polygon with 4 vertices and all corners 90 degrees. This could then be rotated and stretched at will. Paul. -- Paul Johnson UUCP: <world>!mcvax!ukc!gec-mrc!paj --------------------------------!-------------------------|------------------- GEC-Marconi Research is not | Telex: 995016 GECRES G | Tel: +44 245 73331 responsible for my opinions. | Inet: paj@uk.co.gec-mrc | Fax: +44 245 75244