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,INVALID_DATE autolearn=no autolearn_force=no version=3.4.4 X-Google-Language: ENGLISH,ASCII-7-bit X-Google-Thread: 1108a1,93fa00d728cc528e X-Google-Attributes: gid1108a1,public X-Google-Thread: 103376,93fa00d728cc528e X-Google-Attributes: gid103376,public X-Google-ArrivalTime: 1994-10-26 21:16:27 PST Newsgroups: comp.lang.ada,comp.object Path: nntp.gmd.de!xlink.net!howland.reston.ans.net!gatech!newsfeed.pitt.edu!uunet!noc.near.net!ray.com!news.ray.com!news.ed.ray.com!swlvx2!jgv From: jgv@swl.msd.ray.com (John Volan) Subject: Re: Generic association example (was Re: Mutual Recursion Challenge) Date: Wed, 26 Oct 1994 23:21:54 GMT Message-ID: <1994Oct26.232154.29094@swlvx2.msd.ray.com> References: <1994Oct18.221751.15457@swlvx2.msd.ray.com> <38289r$79m@oahu.cs.ucla.edu> <1994Oct19.143843.372@wdl.loral.com> <38fi4r$l81@oahu.cs.ucla.edu> <1994Oct24.203214.4967@swlvx2.msd.ray.com> Sender: news@swlvx2.msd.ray.com (NEWS USER) Organization: Raytheon Company, Tewksbury, MA Xref: nntp.gmd.de comp.lang.ada:16215 comp.object:16656 Date: 1994-10-26T23:21:54+00:00 List-Id: eachus@spectre.mitre.org (Robert I. Eachus) writes: >In article <1994Oct24.203214.4967@swlvx2.msd.ray.com> jgv@swl.msd.ray.com (John Volan) writes: > > One problem I see with this particular formulation is that a > > Person-who-can-occupy-an-Office can point to any Office, not > > necessarily only an Office-that-can-be-occupied; likewise, an > > Office-that-can-be-occupied can point to any Person, not > > necessarily only a Person-who-can-occupy-an-Office. >That is what abstract types are for. In particular, the feature added >last August that abstract types need not have any abstract operations >makes it easy. All the types that you can point to but shouldn't are >abstract, and since the type determination in a dispacthing operation >comes from the object, the issue doesn't arise. Hmmm... Yes, I can see that this guarantees that the only objects that would exist at run-time would be "Persons-who-can-occupy-Offices" and "Offices-that-can-be-occupied-by-Persons", and not just "Persons" and "Offices". Or, more to the point, they could only be "Total Persons" and "Total Offices", supporting all the associations that pertain to "Persons" and "Offices" within the given application. And the objects themselves would "know" what concrete type they were, by virtue of their tags. But I still see a problem: It looks like each class is giving up the ability to use static strong typing to assert the exact type of the *other* objects associated with it. In other words, a "Person-who- can-occupy-an-Office" can't assert that it's associated with an "Office-that-can-be-occupied-by-a-Person" (or with a "Total Office", for that matter). All it can say is that it's associated with an "Office". So when a client asks a Person object for its Office, the Person can answer with its associated "Office", but it can't provide the client with a "Total" view of that Office. The client would have to do the view-conversion itself by "narrowing" (safely downcasting) that Office into a "Total Office". That narrowing operation would incur a run-time check on the Office's tag. But that check is actually redundant. You know and I know that the Office object will really be a "Total Office"; we can assert that as a statically-determined fact, since we're the designers of the software, and that was our original intent for the association. However, when we go to write our Ada code for the Person class, we discover that we can't explicitly assert, in the Ada code itself, that a Person's associated Office will actually be one of those "Total Offices". So we can't tell the compiler that this is a statically-determined assertion that it can check statically. Instead, it becomes a dynamically-checked assertion. In short, this takes us a step away from a statically-checked style of programming, and moves us closer to the dynamically-checked style of languages such as Smalltalk. I suppose we could apply a pragma Suppress at some point to eliminate the extra dynamic check, once the program was fully tested. But, more to the point, we've lost a certain amount of expressibility. Since the Ada code doesn't directly express what we mean, its readability by other Ada programmers (for example, maintainers) is reduced. Moreover, I must insist once again that all these extra layers of abstract types don't really represent anything from the problem domain. As far as our object-oriented analysis is concerned, a Person is just a Person, and an Office is just an Office. Part of what it means to be a Person is the ability to occupy an Office, and likewise, part of being an Office is the ability to accomodate Person (at least within the application in question). All those abstract types layered on top of this are really just artifacts of the way we've chosen to construct the software that will implement our analysis. Perhaps we can exploit all these layered views to gain some reusability benefits, but the costs that they incur are very clear: The job of reading and understanding the program has been complicated by the proliferation of multiple views for what, in the problem domain, are just single classes. You mentioned that you found it difficult to come up with *names* for those multiple views. I've had trouble with that too, as you can see from the way I had to resort to awkward phrases such as "Person-who-can-occupy-an-Office". Far from being a minor inconvenience, I really think this is a diagnostic symptom of a very serious problem. What cannot be easily named, cannot be easily explained, and therefore cannot be easily understood. > > Is there a way of putting this together that guarantees the > > invariant that if a Person occupies an Office, then that Office is > > occupied by that Person? >Yes. It is best understood with the "double generic" version, but >doesn't depend on it. However, make sure you reserve one-to-one >mappings for where they are appropriate. Supporting many-to-many >mappings requires a lot more complexity in the Association >abstraction, so there should probably be several--one-to-one and onto, >one-to-many, and many-to-many. They only need to be written once, so >let's try the simplest case: >(I spent a lot of time barking up the wrong tree on this. Querying >the attributes is not a problem, but defining an operation to set them >resulted in all sorts of visible kludges or silly looking code. There >are two facets to the solution. The first is that in the one-to-one >case there are two necessary set operations: un-set and set, not set >for office and set for person. Yes, I encountered the same issue: I had to have a "Dissociate" operation as well as an "Associate" operation, and the latter wound up calling the former when the cardinality of the association was "one" (but not when it was "many"). >The second is that, while the query >functions want to be class-wide in one parameter, [the Set procedures] >should be >symmetric, and thus class-wide in both.) That's if you're interpreting "Set" and "Unset" (or "Associate" and "Dissociate") as "friend" operations. In other words, you're not considering them as "belonging" to either class specifically, but rather they're sort of straddling the association between the classes. But that's not the only way you could arrange things. If you noticed, in my previous posts, I actually conceived of *distributing* this "Setting" responsibility to *both* of the classes. In other words, I imagined having *two* "Associate" operations: one that was primitive for an Employee and one that was primitive for an Office. The Associate for Employee would only directly modify the Employee's pointer to its Office; likewise the Associate for Office would only directly modify the Office's pointer to its Employee. That follows the "Law of Demeter": an object's operations should only directly manipulate its own attributes, but not the attributes of any other object. However, both of these Associate operations would also be responsible for satisfying the invariant of the association: i.e, if an Employee points to an Office, then that Office must point back to that Employee as well. The most convenient way for the two Associate operations to do that would be ... to call each other! In other words, they would be *mutually recursive* subprograms, reflecting the *mutually recursive* relationship between the two classes. Consequently, we could use either one of those Associate subprograms to establish a link between a given Employee and a given Office. Which one we would pick at any given point would be arbitrary, and would probably depend on which of the two classes we were focusing on in the course of some larger algorithm. The upside to this scheme is that we didn't have to resort to breaking object-oriented encapsulation in order to implement this capability. In other words, we didn't have to make this a "friend" operation that can "see" the structure of both types at once. The downside, though, is that now we have to look at two subprograms, distributed in two packages, to fully understand how the "functionality" of "setting up an association" will get done. But I don't think that's really so bad. As I've said before, it's going to be a very common thing for "functionality" to be smeared across multiple object classes, whenever you make the objects, rather than the functions, the central criterion of your software design. > generic > type Father is abstract tagged private; > -- probably all the abstract types should be limited too. > type Target_Ancestor is abstract tagged private; > -- ancester of the destination type, for example, Controlled. > package Association is > type Extended is abstract new Father with private; > function Get(E: in Extended) return Target_Ancestor'CLASS; ^^^^^^^^^^^^^^^ You see, that's what I mean -- you've lost the ability to express the exact type of the other object in a static fashion, even though you, as the designer, do know what that exact class should be, a priori. > -- If you follow the above directions about abstraction, this > -- must always return the "right" type. But if you have several > -- non-abstract types which are specializations of say Person, > -- you want the attribute declared this way anyway. Yes, I have no trouble with this returning a class-wide type, I just have trouble with the root-type of the class being so indistinct. I'd rather be able to say something like "Internal_Extended'Class" here. But that's the chicken-and-egg problem again. > -- Raises > -- Constraint_Error if the attribute is not set. Interesting interpretation. I didn't think there was any trouble with returning "null". Clients could just interpret a "null" value as meaning that E isn't associated with any Target. But that's okay -- you just have a slightly different abstraction in mind than I have. > function Is_Set(E: in Extended) return Boolean; > -- Inquiry function to avoid Constraint_Error. This might be supplied anyway as a convenience, even if you go with my interpretation of allowing Get to return null. > generic > type Mother is abstract tagged private; > package Inner_Association is > > type Inner_Extended is new Mother with private; > function Get(E: in Inner_Extended) return Extended'CLASS; > -- Again we want the 'CLASS even in cases where it may not be > -- necessary to complete the code... > function Is_Set(E: in Extended) return Boolean; > -- Inquiry function to avoid Constraint_Error as above. > procedure Safe_Set (E: in out Extended'CLASS; > IE: in out Inner_Extended'CLASS); > procedure Force_Set(E: in out Extended'CLASS; > IE: in out Inner_Extended'CLASS); > -- There are two choices here, set in any case, but preserve > -- the invariants, or raise an exception and change nothing if > -- one or the other is already set. Since it is simple to > -- provide both, I do so. (Safe_Set does the checks and may > -- raise an exception, Force_Set unsets the partner of any > -- object that is being reassigned.) Interesting. I hadn't thought of providing these alternatives. My Associate subprograms correspond to your Force_Set. > procedure UnSet(E: in out Extended'CLASS); > procedure UnSet(IE: in out Extended'CLASS); > -- UnSet the attribute. If already set, unset the partner as well. By the way, in the case of "many"-cardinality, I think UnSet would actually wind up being a single operation with two parameters. > private > type Outer_Ref is access all Extended; > type Inner_Extended is new Mother with record > Attribute: Outer_Ref; > end record; > end Inner_Association; > pragma INLINE(Get, Is_Set, Safe_Set, Force_Set); > private > type Inner_Ref is access all Target_Ancestor; > type Extended is new Father with record > Attribute: Inner_Ref; > end record; > pragma INLINE(Get, Is_Set); > end Association; [snip implementation of generic package body] > Okay, now using this package goes like this... > with Ada.Finalization; with Assignments; > package People is > type Base_Person is abstract > new Ada.Finalization.Controlled with private; > package Office_Assignments is > new Assignments(Base_Person,Ada.Finalization.Controlled); ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Once again, it looks like we've lost some expressibility here: At this point, we can't say anything more about what kind of objects People are going to be associated with, other than that they're going to be something derived from Ada.Finalization.Controlled. But you and I know that these are Offices. > > type Person is new Office_Assignments.Extended with null; > function Office(P: in Person) return Controlled'CLASS renames Get; > function Has_Office(P: in Person) return Boolean renames Is_Set; > private > ... > end People; > > with Ada.Finalization; with People; > package Offices is > type Office_Base is abstract new Ada.Finalization.Controlled > with private; > package People_Assignments is new > People.Office_Assignments.Inner_Association(Office_Base); > > type Office is new People_Assignments.Inner_Extended with null; > function Occupant(O: in Office) return > People.Office_Assignments.Extended'CLASS renames Get; > function Is_Occupied(O: in Office) return Boolean renames Is_Set; > procedure Reassign(P: in out People.Office_Assignments.Extended'CLASS; > O: in out People_Assignments.Inner_Extended'CLASS); > ... > private > ... > end Offices; > > If I'm totally mixed-up about mixins :-), please help me out. Thanks. > I hope this helps. The trick is to get as much of the "plumbing" >code into generics which are written once, and then use appropriate >renamings to make it understandable. I fully agree with this sentiment in principle, but not necessarily with the way you've put it into practice here. For example, those mutually-recursive Associate procedures I described above do follow a common pattern that can apply to any association. That makes them an excellent candidate for a generic solution, and I think I've come up with one. However, it only takes care of *implementing* the Associate operations. You still have to write the spec for an Associate procedure in each of your class packages. But when you get to the package bodies, you don't have to worry about doing the "plumbing." You can instantiate the template in terms of those procedure specs, and then, via renaming declarations, you can use the resulting instance procedures as the *bodies* for those specs! Granted, it doesn't take care of all the "boilerplating" that you need to support an association; I think perhaps some code-generating tool might be better suited to doing that, and this generic could could work along with that tool. But using renaming declarations as subprogram bodies is a neat trick that Ada9X now makes possible. >(In fact, in the code above I >probably would use subtype definitions to make those ugly 'CLASS >parameters go away. The other possible approach is to replace the >renamings with operations on the parent types which do the ugly calls >in the body. It's a matter of style and in this case, I'm trying to >show the workings...) >-- > Robert I. Eachus >with Standard_Disclaimer; >use Standard_Disclaimer; >function Message (Text: in Clever_Ideas) return Better_Ideas is... -- John Volan -------------------------------------------------------------------------------- -- Me : Person := (Name => "John Volan", -- Company => "Raytheon Missile Systems Division", -- E_Mail_Address => "jgv@swl.msd.ray.com", -- Affiliation => "Enthusiastic member of Team Ada!", -- Humorous_Disclaimer => "These opinions are undefined " & -- "by my employer and therefore " & -- "any use of them would be " & -- "totally erroneous."); --------------------------------------------------------------------------------