From mboxrd@z Thu Jan 1 00:00:00 1970 X-Spam-Checker-Version: SpamAssassin 3.4.4 (2020-01-24) on polar.synack.me X-Spam-Level: X-Spam-Status: No, score=0.8 required=5.0 tests=BAYES_00, GUARANTEED_100_PERCENT autolearn=no autolearn_force=no version=3.4.4 X-Google-Language: ENGLISH,ASCII-7-bit X-Google-Thread: fac41,c52c30d32b866eae X-Google-Attributes: gidfac41,public X-Google-Thread: 103376,2ea02452876a15e1 X-Google-Attributes: gid103376,public X-Google-Thread: 1108a1,c52c30d32b866eae X-Google-Attributes: gid1108a1,public From: jhd@herold.franken.de (Joachim Durchholz) Subject: Re: Real OO Date: 1996/03/28 Message-ID: <65h8jGx-3RB@herold.franken.de> X-Deja-AN: 144659664 distribution: world references: <4id031$cf9@dayuc.dayton.saic.com> newsgroups: comp.lang.eiffel,comp.lang.ada,comp.object Date: 1996-03-28T00:00:00+00:00 List-Id: ncohen@watson.ibm.com wrote 26.03.96 on Re: Real OO: > The Ada rule under discussion--that a subprogram call is not dispatching > unless the relevant ACTUAL parameters belong to a classwide type--does > not limit the called routine at all. It provides the opportunity for the > CALLER, calling from a context in which the dynamic type is known, to > declare this knowledge. The Eiffel optimization you describe is a > difficult one because in practice it is difficult for an optimizing > compiler to resolve pointer-induced aliasing and determine the dynamic > type at compile time. Eiffel references ( = pointers for this discussion) always have a static type that can be inferred by the compiler. A pointer might be more restricted, but then it should be declared to be of the more restricted type in the beginning. Pointer-induced aliasing is possible in Eiffel (and in fact creates problems in other areas), but even then the referred-to objects is 100% guaranteed to conform to the declared type. No data-flow analysis necessary. > The analysis required is expensive, requiring > interprocedural analysis to be effective, True. And one of the places where a typical Eiffel compiler would spend time. But I don't think this is a real problem - the Eiffel compiler will spend less time than the programmer thinking about wether he can really assume what he's telling the compiler. (And even less time if the programmer goes bug-hunting if the programmer's assumption was wrong.) > and the payoff > is rare, so it is not clear that it is worthwhile for an optimizing > compiler to perform this analysis routinely. My Booch says that it is usually possible to replace 80% of all calls by static (non-polymorphic) ones. If I can guarantee that I catch all cases, this is worthwile. (And Eiffel allows me to statically determine the set of possible dynamic types, so no problem there.) > In Ada, the programmer > conveys, not through any special hint-giving pragma, but just through the > natural course of declaring an object to belong to a specific type, that > a call should not be dispatching. In Eiffel, the programmer "declares" an object to belong to a specific type by not assigning any object of a descendant type to it. I think this is less error-prone and requires less thinking about an otherwise uninteresting detail. > |> > Making it classwide simply documents the fact that the algorithm does > |> > not depend on the tag of the parameter, and causes the compiler to > |> > catch any attempt to override. > |> > |> This assumes that the writer of a module knows that his routines never > |> need to be overridden. > > No, it assumes that the writer of a module knows that a particular > routine in the module accomplishes its purpose without directly > exploiting knowledge of a given object's dynamic type, or tag. (The > routine may invoke other routines that dispatch based on the dynamic > type, but it does not itself differentiate among different dyanamic > types.) Sorry to object, but it *does* assume that the routine will never be overridden. This includes cases where the algorithm in the routine isn't appropriate for some descendant type - if the routine can't be changed (overridden) then, the whole class will have to be implemented. > A classwide routine is, by definition, one whose > correctness does not depend on the dynamic types of the objects it is > dealing with. True. But then, how do I know that? I think cases where I'm sure that this will hold are very rare. And there is always the possibility that somebody will extend the class in a way that I never anticipated, and in such a case, it is quite likely that routines that I considered classwide are anything but classwide in the new design. Besides, I don't think that programmer Joe Average will be able to make this distinction, especially not if the deadline has been yesterday. > ... > That's a rather limited view of the utility of OO techniques! Even fixed > and well understood tasks benefit from data abstraction and > encapsulation, from the use of inheritance to avoid redundant effort, and > from the use of polymorphism to facilitate generalized algorithms. Agreed. Though this won't persuade a hard-headed Structured Programming proponent. Hell, IBM successfully markets RPG, which isn't even structured, and lots of programs are being written in it. > Concerning dispatching based on multiple parameters: > > |> The type of dispatching noted here can easily be achieved with anchored > |> types in Eiffel. In an Ada-style notation, this would look as follows: > |> > |> function Corresponding_Parts_Equal > |> (Self: Shape_Type; Other: like Self) > |> return Boolean > |> > |> The "like Self" part requires Other to have the same dynamic type as > |> Self, or a type that is a descendant of Self's type. > > (Are you sure it's the DYNAMIC type? Section 11.4.3 of Meyer's > _Object_Oriented_Software_Construction_ seems to suggest that it's the > declared type.) Yes. It follows from the fact that the definition is automatically rewritten in each inheriting class. I.e. 'like Current' makes sure that dynamic and static types always match, because it causes the definition to be implicitly rewritten for each descendant class by the compiler. (Of course the compiler doesn't generate code for this in descendant classes; after all, the descendant types are just pointers behind the scene, so the original routine will still work.) > The following calls violate the requirement that the dynamic type of > Other be descended (in zero or more steps) from the dynamic type of Self, > and presumably result in a run-time error: > > FC.Corresponding_Parts_Equal(C) > HC.Corresponding_Parts_Equal(C) > FC.Corresponding_Parts_Equal(HC) > HC.Corresponding_Parts_Equal(FC) Yes, these calls are invalid. (Though they won't cause run-time errors - the compiler will find and flag them. It can do that because it knows the static type of the left-hand object and so knows the static type of the formal parameter for that call.) Umm I'm just noting this exposes a fatal flaw in my "anchored" 'like Shape_Type' definition. Let's say I'm keeping a list of Shape_Type objects (so that the compiler won't know the dynamic subtypes), the call to Corresponding_Parts_Equal would be rejected by the compiler (this is suitably named a 'catcall', 'cat' meaning 'changed availability or type - the 'like Current' anchor redefines the type for all descendant classes of Shape_Type, resulting just in this type of problem). So I have to shamefully retract my proposal of using 'like Current'. > The problem with this assymetry is not merely aesthetic: It greatly > complicates the programming of a routine designed to exploit the "or a > type that is a descendant of" property of Corresponding_Parts_Equal, > because that routine must determine which object's type, if any, is at > least as general as the other object's type. True. And determining which of two objects is more general makes Eiffel code that needs some heavy commenting (i.e. that is ugly, obfuscated, and inelegant). > type Graphic_Type is tagged > record > Shape : Shape_Type; > Fill : Fill_Type; > end record; > If one is interested in congruence of Graphic_Type instances, that's a > very different notion from congruence of abstract geometric figures, but > it's easily implemented as a classwide operation: > > function Congruent > (Graphic_1, Graphic_2: Graphic_Type'Class) return Boolean is > begin > return Congruent (Graphic_1.Shape, Graphic_2.Shape); > end Congruent; Plus redefining all routines of Shape_Type for Graphic_Type, with similar one-liners. And the Graphic_Type class would have to be revisited whenever the Shape_Type class is extended with new functionality, to include the new features. I'd think this is a classical example for when to use inheritance . > Nonetheless, let's persue the consequences of trying to extend > Circle_Type (a descendant of Shape_Type) to include such subclasses as > Filled_Circle_Type and Hatched_Circle_Type. > > ... > > |> In addition, the Congruent code will break - Shape1'Tag and Shape2'Tag > |> will not be equal, making Congruent return false even if a Filled_Circle > |> and a Hatched_Circle actually have equal corresponding parts. > > True. If I had intended to allow shapes to be specialized to include > nongeometric features, I would add a new abstract method to Shape_Type: > > function Geometry (Special_Shape: Shape_Type) return Shape_Type'Class > is abstract; > > I would override it for Circle_Type as follows: > > function Geometry (Special_Circle: Circle_Type) return Shape_Type'Class > is Result: Circle_Type := Special_Circle; > begin > return Result; > end Geometry; > > This function always returns an object whose dynamic type, or tag, is > that of Circle_Type. Types extending Circle_Type would inherit this > function without overriding it. Thus if FC is a Shape_Type'Class object > with the dynamic type (tag) of Filled_Circle_Type, Geometry(FC) would > dispatch to the function body above and would return the corresponding > (truncated) Circle_Type object. Similar definitions of Geometry would be > given for all types in the hierarchy containing only geometric > information (e.g., Rectangle_Type, but not Filled_Rectangle_Type). > > The Congruent function would be rewritten as follows: > > function Congruent (Shape_1, Shape_2: Shape_Type'Class) return Boolean is > Shape_1_Geometry : Shape_Type'Class := Geometry(Shape_1); > Shape_2_Geometry : Shape_Type'Class := Geometry(Shape_2); > begin > return > Shape_1_Geometry'Tag = Shape_2_Geometry'Tag and then > Corresponding_Parts_Equal(Shape_1_Geometry, Shape_2_Geometry); > end Congruent; > > This function will return True when invoked with a filled circle and a > hatched circle having the same radius. I'm not sure whether this has a problem. I feel a bit uneasy about it, but I can't quite put my finger on it. I suspect this might cause problem later, and in cases that go beyond the simple example we're discussing here, but I'm not sure. Still, I have to admit it does the job, in a way that I wouldn't have thought of. -Joachim -- Im speaking for myself here.