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: 103376,93fa00d728cc528e X-Google-Attributes: gid103376,public X-Google-Thread: 1108a1,93fa00d728cc528e X-Google-Attributes: gid1108a1,public X-Google-ArrivalTime: 1994-10-31 10:57:44 PST Newsgroups: comp.lang.ada,comp.object Path: bga.com!news.sprintlink.net!hookup!news.mathworks.com!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: Mon, 31 Oct 1994 17:34:04 GMT Message-ID: <1994Oct31.173404.22096@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> <1994Oct26.232154.29094@swlvx2.msd.ray.com> Sender: news@swlvx2.msd.ray.com (NEWS USER) Organization: Raytheon Company, Tewksbury, MA Xref: bga.com comp.lang.ada:7400 comp.object:8073 Date: 1994-10-31T17:34:04+00:00 List-Id: eachus@spectre.mitre.org (Robert I. Eachus) writes: > In article <1994Oct26.232154.29094@swlvx2.msd.ray.com> jgv@swl.msd.ray.com (John Volan) writes: > [snip my objection about the need for the client to do narrowing to get a fuller view of an associated object after a query] > Let the compiler do the work. If there is only one concrete type >in an abstract class, the check might result in a call at run-time to >a check, but the check should be implemented as a return instruction. >If there are types derived from Office or Person, there may be an >actual check in some other cases that you want made. But any compiler >that does run time checks for abstract types, or builds dispatch >tables for abstract types has a bug. (There are cases where you may >have a view of an object as an abstract type, but that must always be >a static view--at least, that is the intent.) It still bothers me that when you query an object like an Employee for another object associated with it, let's say its Office, the result you get back will only provide a highly abstract view of that associated object. If the client wants to invoke operations on the second object that are only available from a more complete view (for instance, we want to find out the Building the Office is in) the client has to do a narrowing type-cast on it. I just think it would have been nice if the original query operation provided the more complete view of the associated object in the first place. But to do that, you somehow have to solve the chicken-and-egg problem. To be fair, even under my generic scheme, an associative query is only going to provide a "highly abstract view", and the client still has to do a conversion to a more complete view. But that "highly abstract view" is one of those Identity.Value types, so the effect is a bit different. An Identity.Value type is just a "black box" with a one-to-one relationship with a particular access-to-classwide-type, and that access type presumably is exactly the "complete" view we're looking for. In theory, the translation shouldn't need to involve any run-time checks -- but, of course, that depends on how you actually implement the "black box." > > What cannot be easily named, cannot be easily explained, and > > therefore cannot be easily understood. > The style of Ada 9X programming I am focusing on addresses this by >making that disappear from sight. The only objects visible in the >"main" program should be complete objects of complete types. The rest >should be treated like making sausage or scrapple and hidden in >private parts whereever possible. Okay, okay, I believe you. I guess it would help me to see a completely worked-out example (or to sit down and work one out myself). Believe me, I'm very enthusiastic about any scheme that can let you snap together a class from reusable parts. But I don't see how the association-supporting code for a class can actually be hidden away in private parts, regardless of how it gets constructed. Remember, I'm working under the basic assumption that association-related operations are going to be public features of a class, in whatever complete view you have for it. I can see that the final step in constructing a class with associations would be to just derive a final concrete type (with null extension) from the last abstract type coming out of a chain of these association-instantiations. But this means that a reader must examine this whole chain of instantiations to fully understand the class. Yes, I see how that might be mitigated by using renamings and subtypes to restate the total class interface accumulated from all the abstract ancestors.... Hmmm ... those renamings you allude to, do you mean them to be renamings-as-declarations, or renamings-as-bodies? I thought you meant the former, but if you meant the latter, then that opens up a whole new realm of possibilities. One upshot of this is that it would at least be conceivable that we could merge our two techniques: Your mixin scheme could be used to easily snap together the implementation backbone for the associations, while my scheme of deferred-coupling-via-generics could be used in packaging up the final "total" abstraction for the classes, solving the chicken-and-egg problem of how to establish mutually-dependent package specs (that is, if you really *need* the mutual dependency to be publicly visible in the package specs). [snip my sketch of mutually-recursive Associate procedures] > Excellent reasoning, but I wouldn't code the blasted bodies that >way. Then perhaps a scheme such as yours could provide the actual hidden implementation. >Infinite recursion is frowned upon, so if set for office calls >set for person and vice-versa, something has to break the loop. Of course. >The >"best" way I found was to set the local attribute, then check the >remote attribute and if it was "wrong" call the matching set. It >works, but it is pretty kludgy. Actually, the way I worked it was to always keep things as "local" as possible: I'd check the local attribute first to see if it was already "right", and then break the infinite recursion there (on the assumption that the mutual-pointing invariant condition had always been satisfied in the past). Otherwise, I'd set the local attribute (disassociating the old partner first if necessary), and then make the mutually-recursive call to the cross-side Associate procedure. The same thing would happen on the other side, and then the first Associate would get called again. At that point, the recursion would stop, because the local attribute would turn out to be "right" already. Is this better or worse than the way you describe? I can't really say, but I'd welcome any comments. >I'm willing to pay a bit for >elegance, but that's a little expensive. Above you talk about >eliminating redundant checks, here you not only prohibit simple >operations from being effectively inlined, but require a check that >the thing you just set has the right value. Well, I can't deny the expense of the mutual recursion. And I'm not sure how I could get rid of the extra check without breaking my own encapsulation scheme: To do that, each class would have to publish a non-recursive "Set" operation that merely set the local pointer without guaranteeing the invariant condition. This operation could be called by the "Associate" operation on the other side in order to fulfill the invariant in an efficient manner. But this opens up the possibility of other clients also calling these weaker "Set" operations, rather than the stronger "Associate" operations, thereby possibly violating the invariant. Obviously not a good idea. (There might be a tricky way to use generics and access-to-subprogram types to make these "Set" operations accessible from the cross-side "Associate" operations without otherwise making them public globally, but this still won't solve the inlining issue you raise.) As for mutual recursion preventing a "simple" operation from being inlined ... hmmm ... that observation is actually triggering some interesting questions in my mind. Let me respond to this first by saying that, yes, you're right, mutual recursion is a liability -- if the operation of setting up an association is really a "simple" one. But what if it isn't "simple"? This question is actually forcing me to rethink some of my own intuitions about this whole issue. You see, in previous posts I myself agreed to the idea that the attributes and operations supporting a given association would probably be class-wide, and orthogonal to those of any other association. In other words, there was probably no need to make these into dispatching operations. And you probably wouldn't want subclasses to alter the way these operations behaved by introducing overriding implementations for them. In fact, I also conceded the possibility that, for some applications, association operations didn't even need to be primitive or inheritable. That's why it was a viable alternative to off-load these operations to child units, where the "withing" problem disappears. Or even to off-load the whole association itself into an association package. But now I'm beginning to think that there may be applications where those assumptions are actually invalid. In fact, there might be occasions where you *do* want to make association operations into primitive, inherited-but-overridable, dispatching operations. I think I've been subconsciously groping toward that intuition all this time, without realizing it. Oh, don't get me wrong: *Structurally*, I don't think a subclass should do anything different about setting up the pointers for an association. One way or another, association operations in a subclass should satisfy the mutual-pointing invariant condition, most likely by just invoking inherited forms of these operations. (For that matter, a hidden implementation for these inherited operations could take advantage of your mixin technique.) BUT, beyond purely structural considerations, a subclass might introduce other *behavioral* variations on what happens when an association is set up (or taken apart). In fact, these differences may involve interactions between the association in question and _other_ _associations_ _the_ _class_ _is_ _involved_ _in_. In other words, associations might not always be so orthogonal after all. As an example, consider these possible application requirements: "Whenever any Employee is assigned a new Office, the Manager who immediately supervises that Employee shall be notified of the move (via an appropriate Memo generated by the system)." "Whenever a Manager is assigned a new Office, all the Employees who are immediate subordinates of that Manager shall be notified of the move (via an appropriate Memo generated by the system)." Here we have a situation where an event affecting one association (Employee gets assigned a new Office) can involve traversing another association (find and notify the Employee's supervising Manager), and can involve additional behavior at a subclass level (if an Employee happens to be a Manager, also find and notify that Manager's subordinate Employees). But if this kind of situation is the case, then the whole issue of inlining becomes a moot point. You can't do a statically-determined optimization like inlining if you're in a dynamically-determined situation like dispatching. Moreover, consider this possibility: "Offices shall be distinguished into Doorless_Offices (i.e., industrial "cattlepens" formed out of modular partitions) versus Doored_Offices (actual rooms with solid walls and doors). Since the latter are highly desirable these shall be preferentially assigned to Managers. There shall be a prioritized list of Managers waiting for Doored_Offices. Whenever a Doored_Office becomes available, either because of new construction/remodeling, or because a previous occupant Employee moves out, the next Manager on the waiting list shall be notified (via an appropriate Memo generated by the system)." Now, when we disassociate an Employee from an Office, we not only need to dispatch on the type of Employee, but we also need to dispatch on the type of Office. The fact that there are *two* classes to dispatch on makes mutual recursion even more necessary -- such an operation would be a "multimethod". It's illegal in Ada9X to have a single operation dispatch against more than one class. But it is perfectly okay to have two operations working in concert, each operation dispatching against only one of the classes. > According to ME ;-) the association package "owns" the Attribute >fields and is the only one to monkey with them, and this is the right >and proper approach in Ada 9X. It is different, but it seems better >from an encapsulation point of view. I agree ... as long as the association truly is a separate abstraction within the problem domain, one that is indeed orthogonal to any other. But I think that there might be some applications where this is not the case. Or rather, only the *structural* aspects of an association might be truly orthogonal and separable, but the overall *behavior* connected with an association might not be. In other words, I'd say your technique is *a* "right and proper Ada9X approach", but not necessarily *the* "right and proper Ada9X approach" for all applications. Be careful: We've been criticizing other languages for forcing us to accept only "One True Way". Let's not simply replace "THEIR One True Way" with "OUR One True Way." Ada is a general purpose language with a lot of expressive power. Its features ought to be able to support many different styles of design, and even allow the fruitful merging of different styles. >With the Smalltalk or C++ model >large and complex object implementations soon get buried in >interactions between different components of an object. In this >model, each abstraction takes care of its own. And I think this is a very good thing, a true benefit of your technique ... as long as these associations are truly separable abstractions. But there may be situations where they would not be separable, and only the classes themselves would be truly separable as abstractions. In such cases, the reason we couldn't address the association operations totally in isolation wouldn't be because of any artefact of our chosen style of software design, but because of the irreducible complexity of the problem domain itself. As Brooks said, there's "No Silver Bullet" that can eliminate that sort of complexity, it just has to be dealt with. We've been staring for so long at this wall of "how to get two types in two different packages to point to each other", that we've forgotten that there may be other walls we need to cut through -- and even some floors and ceilings, too, if we want to install stairwells and elevators. My deferred-coupling-via-generics technique really addresses a broader set of problems than just mutually-pointing types. It provides a solution for any situation where two or more abstractions need to be mutually dependent on each other, and the dependency needs to be a public feature of their respective interfaces (and perhaps even a primitive feature), yet the different abstractions still need to be encapsulated in separate packages. > I left some stuff out here for simplicity, but in a "real" >implementation of this idea, I would insist that both object be >derived from Controlled, or Limited Controlled, Total agreement here. >and I would insure >that there were no dangling pointers when an object was destroyed, and >that copying an object did not copy the assignments. (In a many-to-one >or many-to-many implementation the policy would be different of >course.) In other words, fix all those bugs before they occur, and >transparent to the user of the abstraction. Couldn't agree with you more. [snip] > > 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. > Major difference, but subtle in its way. I hid the "pointer" >types, so all external interfaces deal with objects, not pointers to >objects. Yes, we know that what gets passed around is a reference, >so there is no extra calling overhead, In that case, it is quite a major difference. My assumption was that a "Get" operation would return a value that gave complete access to the associated object as a *variable*, so that a client doing a traversal would then be able to invoke operations that could modify the state of that object, if need be. In other words, the return value would be a "name" that designated an object, rather than the "value" (state) of object itself. Since it is possible that there might not yet be any object assigned along a given association (e.g., a particular Employee might not have an Office yet), it is possible that the "name" retrieved by a Get would be "null" (or the moral equivalent of that), designating "no object at all." Now, I may be confused, but I always thought that a function call could only act as the name of a *constant*, not the name of a variable. The only way the return result of a function call could designate a variable would be if the return type were actually an access type (or something readily translatable into one, such as one of my Identity.Value types). Has this changed in Ada9X? From what I can find in the RM9X;5.0, it seems not. Unless your "object" types themselves act as the moral equivalent of access types, magically designating your objects rather than being the objects themselves, I think that the usefulness of your Get functions will be very restricted. In other words, your tagged types would have to implement "reference semantics", in a way similar what Bill Beckwith does in his IDL-to-Ada9X translation. But if we're dealing with reference semantics anyway, then the special value "reference to no object at all" ought to be part of that semantics. Taking a closer look at your code, I notice that your object types are non-limited tagged types, rather than limited tagged types. I thought Ada9X could only guarantee pass-by-reference for a limited type, whereas a non-limited type may be either pass-by-copy or pass-by-reference. >but doing it this way >eliminates--if done correctly--dangling pointer worries for users of >the abstractions. I didn't show all of that, but for example, the >finalization operation on a Person would unset his office. So there >is no null value to return. I'm not sure what the issue of "dangling pointers" has to do with the choice of whether to hide the use of pointer types or expose the possibility of null pointers. I always thought that "dangling pointers" referred to a situation where one object has already been destroyed (e.g., via Unchecked_Deallocation), yet some other object still has an access value designating it. Certainly, the process of finalizing any object should include disconnecting it from every association it takes part in, so that the associated objects will no longer retain pointers to it. But doesn't that mean that those other objects will then contain null pointers (assuming the cardinality is "one")? > Robert I. Eachus John G. 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."); --------------------------------------------------------------------------------