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, T_FILL_THIS_FORM_SHORT autolearn=no autolearn_force=no version=3.4.4 X-Google-Language: ENGLISH,ASCII-7-bit X-Google-Thread: 103376,7a4380ff535aa24d X-Google-Attributes: gid103376,public X-Google-ArrivalTime: 1994-12-05 06:18:48 PST Newsgroups: comp.lang.ada Path: bga.com!news.sprintlink.net!hookup!news.mathworks.com!news2.near.net!noc.near.net!ray.com!news.ray.com!news.ed.ray.com!swlvx2!jgv From: jgv@swl.msd.ray.com (John Volan) Subject: Re: cross linking packages Sender: news@swlvx2.msd.ray.com (NEWS USER) Organization: Raytheon Company, Tewksbury, MA Message-ID: References: Date: Mon, 5 Dec 1994 13:30:58 GMT Date: 1994-12-05T13:30:58+00:00 List-Id: rburema@inter.NL.net (Rene Burema) writes: >Dear netlanders, > >I have a problem specifing a seemingly simple problem in Ada. Anybody >want to help? > >The problem is as follows: > >-------------------- package A ----------------------- >package A; >type tA is private; >procedure giveB(x: in tA,y: out tB); >end A; >-------------------- package B ----------------------- >package B; >type tB is private; >procedure giveA(x: in tB,y: out tA); >end B; >--------------- >where tA could be a reference to a Doctor giving the reference to >a Patient (tB) and vice versa. > >I need a with statement for B and A respectively. The Ada gnat compiler >however stumbles into a circular dependency. How can I solve this without >placing the two types tA and tB in a single package. > >Many thanks for any help > > Arjen Duursma (arjen@capints.uucp) > >PS: don't use the return adress in the message, as it is a shared account. (To quote the bowl of petunias that materialized 100 miles above the surface of Magrathea: "Oh no, not again!" :-) Arjen, I would characterize this as the problem of establishing "separately-encapsulated," yet "mutually-coupled" classes of objects. On the one hand, we'd like to be able to separately encapsulate different object classes as tagged private types in different packages, thereby fully exploiting Ada's mechanisms for information hiding. On the other hand, we occasionally need mutual coupling between classes even at their *interface* level: In other words, class A would have primitive subprograms that include parameters of class B -- and vice versa. Folks like Bill Beckwith call this the "withing" problem, because in Ada it is impossible to have two package *specs* that both "with" each other. (Although it's perfecty okay to have two package *bodies* "with" each other's specs.) This problem is also known as Ada's "chicken-and-the-egg" problem. A couple of months ago, I kicked off a rather long-winded thread concerning this very subject. At the end if it all, I believe I was able to find a reasonable Ada9X workaround (described below) that achieves what you are looking for. Unfortunately, during that thread I focussed on a particularly strong case of "mutual coupling" -- namely, object classes that formed "mutually-recursive" data structures (where objects of different classes actually contain pointers to each other). Many participants in that thread focussed on the mutually-recursive data structure to the exclusion of all else, insisting that such a data structure represented a single, undecomposable abstraction -- so there would supposedly be no point in trying to "separately encapsulate" the classes anyway. I kept insisting that "separate encapsulation" is both desirable and feasible, even under these circumstances. But this issue seemed to get lost in all the verbiage being slung back and forth about mutual recursion. I view the problem of "separately-encapsulated" yet "mutually-coupled" classes as really being an issue of "deferred coupling": We need to couple two or more classes together, but if we want to keep them separately encapsulated we are forced to somehow *defer* that coupling until *after* all their interfaces can be established. One way or another (short of a language change), achieving this deferred coupling will require some special tricks and some extra work. Most of the Ada9X cognoscenti in this newsgroup seem to favor exploiting abstract types and inheritance as a mechanism for "deferred coupling." But, IMHO, I feel this is an "improper" use (and perhaps even an outright abuse) of the mechanism of inheritance. (I think it's a symptom of "Hammer Syndrome", as in the proverb "When your favorite tool is a hammer, every problem starts to look like a nail." :-) Personally, I prefer achieving deferred coupling by exploiting other mechanisms that Ada9X provides (see below), and I would rather reserve inheritance for what it was originally "intended" for: representing true generalization/specialization relationships that come directly from the problem domain. A discussion of my technique follows. I hope this helps you out. -- John Volan (LONG POST WARNING. ^L inserted for all you tired souls who just want to hit 'n' now and have done with it :-) ================================================================================ "SEPARATELY-ENCAPSULATED" YET "MUTUALLY-COUPLED" OBJECT CLASSES IN Ada 9X ------------------------------------------------------------------------- One possible solution: Use an extra level of indirection, and establish your object classes in *three* steps ("Identity, Interface, and Implementation") rather than just the classic two steps ("Interface and Implementation"). Step 1: ESTABLISH THE CLASS "IDENTITIES": First, "forward-declare" each class. In other words, for every class X, establish that the class will *exist*, but do so before actually specifying the *interface* to class X. Do this by introducing a package X_Id, containing little more than a private type X_Id.Val. A value of type X_Id.Val will represent an "opaque identity value" for an X object. In other words, it will somehow "designate" or "reference" or "point to" an X object. However, at the moment, such references will be "opaque", because we haven't actually declared the type for X objects themselves, yet. We do promise to eventually provide conversions between X objects and these X "identity values", but we have to defer that until later, once we've had a chance to establish the interface to the X class. We also promise to preserve type-safety: X_Id.Val's will *only* be interconvertible with X objects, and *not* with any other classes of object. The only way to generate an X_Id.Val will be from an access value designating an X object. And the only thing we'll be able to extract out of an X_Id.Val will be an access value designating an X object. But again, all this is deferred until we can actually declare the X object type. (Depending on how we implement the X_Id.Val types, guaranteeing type-safety may be a matter of programmer discipline. Getting the language to 100% guarantee type safety here may be tricky -- but more about this issue later.) This step is really pretty simple. For instance, let's say we're going to have a Doctor class and a Patient class (and maybe other classes, too). Here's how we could "forward-declare" these classes: ---------------------------------------------------------------------- package Doctor_Id is type Val is private; -- That's all you need, really (for now). private type Val is ... -- more about implementation in a while end Doctor_Id; ---------------------------------------------------------------------- package Patient_Id is type Val is private; -- That's all you need, really (for now). private type Val is ... -- more about implementation in a while end Patient_Id; ---------------------------------------------------------------------- ... -- similar packages for other classes Step 2: ESTABLISH THE CLASS "INTERFACES": Next, write the "interfaces" for your classes: For each class X, introduce a package spec X, containing a tagged type X.Obj (it can be private, limited, abstract, however you please) that actually represents your X objects. Declare all the appropriate primitive subprograms for your class. This step is not much different than what people normally do when they want to introduce an object-oriented type in an Ada package. However, there is one difference: Wherever one of these primitive subprograms needs a parameter of some *other* class Y, don't try to "with" the spec of Y. Instead, "with" the corresponding Y_Id package, and then declare parameters of type Y_Id.Val. That way, you don't have to wait until the interface of class Y is established, before establishing the interface of class X. Yet this still lets you specify that class X and Y are coupled. And if X and Y need to be *mutually* coupled, this is not a problem: The *interfaces* of the two class packages don't need to "with" each other -- they only need to "with" each other's *identity* packages. Also, fulfill the promise that you made when you introduced the X_Id package: Declare a general-access-to-classwide-type X.Ptr, and then declare functions that allow free interconversion between "opaque X identity values" (X_Id.Val's) and "transparent X pointer values" (X.Ptr's). For instance, let's say we have class Doctor and class Patient (and perhaps other classes, as well). Let's say that class Doctor has a method called Doctor.Treat_Patient, class Patient has a method called Patient.Receive_Treatment, and Doctor.Treat_Patient must call Patient.Receive_Treatment. At the same time, class Patient has a method called Patient.Pay_Doctor, class Doctor has a method called Doctor.Receive_Payment, and Patient.Pay_Doctor must call Doctor.Receive_Payment. Consequently, we have a mutual-coupling situation between the two classes. But since the "existence" of the two classes has already been established via their Id packages, establishing this mutual coupling at the interface level is not a problem: ---------------------------------------------------------------------- with Doctor_Id, Patient_Id, ... ; -- possibly other stuff, too package Doctor is type Obj is ... tagged ... ; -- abstract, limited, private, what have you procedure Treat_Patient (The_Doctor : in out Doctor.Obj; The_Patient_Id : in Patient_Id.Val); procedure Receive_Payment (The_Doctor : in out Doctor.Object; From_Patient_Id : in Patient_Id.Val); ... -- possibly other Doctor primitives type Ptr is access all Doctor.Obj'Class; -- fulfill the "promise" function To_Id (The_Doctor_Ptr : in Doctor.Ptr) return Doctor_Id.Val; function To_Ptr (The_Doctor_Id : in Doctor_Id.Val) return Doctor.Ptr; ... end Doctor; ---------------------------------------------------------------------- with Patient_Id, Doctor_Id, ... ; -- possibly other stuff, too package Patient is type Obj is ... tagged ... ; -- abstract, limited, private, what have you procedure Pay_Doctor (The_Patient : in out Patient.Obj; The_Doctor_Id : in Doctor_Id.Val); procedure Receive_Treatment (The_Patient : in out Patient.Object; From_Doctor_Id : in Doctor_Id.Val); ... -- possibly other Patient primitives type Ptr is access all Patient.Obj'Class; -- fulfill the "promise" function To_Id (The_Patient_Ptr : in Patient.Ptr) return Patient_Id.Val; function To_Ptr (The_Patient_Id : in Patient_Id.Val) return Patient.Ptr; ... end Patient; ---------------------------------------------------------------------- ... -- similar package specs for other classes Step 3: ESTABLISH THE CLASS "IMPLEMENTATIONS": Finally, flesh out the "implementation" for each class X, in its corresponding package body X. For every other class Y which class X needs to interact with, go ahead and "with" the spec of the corresponding package Y. Even if there is a mutual-coupling situation between X and Y, there is no problem: It's always been possible for the *bodies* of two packages to "with" each other's specs (even in Ada 83). Thus, we have managed to achieve deferred coupling: We were able to wait until the *bodies* of these packages to actually couple them together. If an X subprogram includes a parameter of type Y_Id.Val, and it needs to invoke some Y subprogram upon the Y object designated by this Y_Id.Val, then make use of the conversion function Y.To_Ptr to get access to the actual Y object. If the Y subprogram needs to be passed a parameter of type X_Id.Val, make use of the conversion function X.To_Id to pack an X'Access into an "opaque" X identity value. Finally, provide some implementation for those X.Ptr <=> X_Id.Val interconversion functions. (More about how to achieve this in a while.) For instance, here's how we can implement the bodies of Doctor and Patient, including getting Doctor.Treat_Patient to call Patient.Receive_Treatment, and getting Patient.Pay_Doctor to call Doctor.Receive_Payment: ---------------------------------------------------------------------- with Patient, ... ; -- possibly other stuff, too package body Doctor is procedure Treat_Patient (The_Doctor : in out Doctor.Obj; The_Patient_Id : in Patient_Id.Val) is The_Patient : Patient.Obj'Class renames Patient.To_Ptr(The_Patient_Id).all The_Doctor_Id : constant Doctor.Id := Doctor.To_Id(The_Doctor'Access); begin ... -- possibly do other stuff with The_Doctor Patient.Receive_Treatment (The_Patient, From_Doctor_Id => The_Doctor_Id); end Treat_Patient; procedure Receive_Payment (The_Doctor : in out Doctor.Object; From_Patient_Id : in Patient_Id.Val) is begin ... -- do whatever end Receive_Payment; ... -- possibly other Doctor primitives -- more about these in a moment: function To_Id (The_Doctor_Ptr : in Doctor.Ptr) return Doctor_Id.Val ... function To_Ptr (The_Doctor_Id : in Doctor_Id.Val) return Doctor.Ptr ... end Doctor; ---------------------------------------------------------------------- with Doctor, ... ; -- possibly other stuff, too package body Patient is procedure Pay_Doctor (The_Patient : in out Patient.Obj; The_Doctor_Id : in Doctor_Id.Val) is The_Doctor : Doctor.Obj'Class renames Doctor.To_Ptr(The_Doctor_Id).all The_Patient_Id : constant Patient.Id := Patient.To_Id(The_Patient'Access); begin ... -- possibly do other stuff with The_Patient Doctor.Receive_Payment (The_Doctor, From_Patient_Id => The_Patient_Id); end Pay_Doctor; procedure Receive_Treatment (The_Patient : in out Patient.Object; From_Doctor_Id : in Doctor_Id.Val) is begin ... -- do whatever end Receive_Treatment; ... -- possibly other Patient primitives -- more about these in a moment: function To_Id (The_Patient_Ptr : in Patient.Ptr) return Patient_Id.Val ... function To_Ptr (The_Patient_Id : in Patient_Id.Val) return Patient.Ptr ... end Patient; ---------------------------------------------------------------------- ... -- similar package bodies for other classes ================================================================================ IMPLEMENTATION NOTES: How do we actually implement those X_Id.Val <=> X.Ptr interconversions? VISIBILITY ISSUE: Before considering mechanisms for doing this, let's first tackle the issue of visibility. These functions need to have visibility to the full implementation of both X_Id.Val and X.Ptr. One way we might solve this would be to introduce a child package called, say, X_Id.Conversion, and have it "with" spec of package X. This gives X_Id.Conversion visibility both to the full X_Id.Val type and the X.Ptr type. X_Id.Conversion could then provide implementations for To_Id and To_Ptr. The *body* of the X package could then "with" X_Id.Conversion and could then use the functions from there as the actual implementations for X.To_Id and X.To_Ptr, via renaming-as-body declarations. So, for instance: ---------------------------------------------------------------------- with Doctor; package Doctor_Id.Conversion is function To_Id (The_Doctor_Ptr : in Doctor.Ptr) return Doctor_Id.Val; function To_Ptr (The_Doctor_Id : in Doctor_Id.Val) return Doctor.Ptr; end Doctor_Id.Conversion; ---------------------------------------------------------------------- package body Doctor_Id.Conversion is function To_Id (The_Doctor_Ptr : in Doctor.Ptr) return Doctor_Id.Val is begin return ... -- whatever it takes to convert a Doctor.Ptr into a -- Doctor_Id.Val, knowing the implementation of both end To_Id; function To_Ptr (The_Doctor_Id : in Doctor_Id.Val) return Doctor.Ptr ... begin return ... -- whatever it takes to convert a Doctor_Id.Val into a -- Doctor.Ptr, knowing the implementation of both end To_Ptr; -- (more about these function in a while...) end Doctor_Id.Conversion; ---------------------------------------------------------------------- with Doctor_Id.Conversion; with Patient, ...; -- and whatever else, as before package body Doctor is ... -- all the Doctor primitives, as before function To_Id (The_Doctor_Ptr : in Doctor.Ptr) return Doctor_Id.Val renames Doctor_Id.Conversion.To_Id; function To_Ptr (The_Doctor_Id : in Doctor_Id.Val) return Doctor.Ptr renames Doctor_Id.Conversion.To_Ptr; end Doctor; ---------------------------------------------------------------------- ... -- similarly for Patient, and any other class In other words, Doctor.To_Id and Doctor.To_Ptr just become convenient repackagings of Doctor_Id.Conversion.To_Id and Doctor_Id.Conversion.To_Ptr, respectively. Unfortunately, this scheme is a bit complicated, and it adds extra entries into our Ada library, which we now have to manage. Another way to attack this would be to exploit generics. You should already have noticed that there is a lot of repetitiveness in all those X_Id packages. That makes them a prime candidate for some reusable generic solution. Indeed, we can easily genericize the notion of an Id package. Moreover, we can even genericize the notion of a providing a Conversion package that can be instantiated at a later time, once there's an Obj and Ptr type available. To achieve all this, I favor using nested generics: -------------------------------------------------------------------- generic package Id is type Val is private; generic type Obj (<>) is abstract tagged limited private; type Ptr is access all Obj'Class; package Conversion is function To_Id (The_Ptr : in Ptr) return Id.Val; function To_Ptr (The_Id : in Id.Val) return Ptr; end Conversion; private type Val is ... ; -- still holding my cards close to the vest for now :-) end Id; ---------------------------------------------------------------------- Thus, instead of having to laboriously spell out all those X_Id packages, we can get the same effect by just doing some quick instantiations: ---------------------------------------------------------------------- with Id; package Doctor_Id is new Id; ---------------------------------------------------------------------- with Id; package Patient_Id is new Id; ---------------------------------------------------------------------- ... -- etc for other classes We could have made Id.Conversion a generic child package, but that would only force us to instantiate it as a library-level child package. With this nesting, we can choose to instantiate X_Id.Conversion right in the spec of package X if we wish, and then directly declare X.To_Id and X.To_Ptr as renamings. For instance: ---------------------------------------------------------------------- with Doctor_Id, Patient_Id, ... ; -- etc, as before package Doctor is type Obj is ... -- as before ... -- primitive Doctor subprograms, as before type Ptr is access all Obj'Class; package Conversion is new Doctor_Id.Conversion (Doctor.Obj, Doctor.Ptr); function To_Id (The_Doctor_Ptr : in Doctor.Ptr) return Doctor_Id.Val renames Conversion.To_Id; function To_Ptr (The_Doctor_Id : in Doctor_Id.Val) return Doctor.Ptr renames Conversion.To_Ptr; ... end Doctor; ---------------------------------------------------------------------- Then there would be no need for any bodies for To_Id and To_Ptr within the body of package Doctor. TYPE-SAFETY ISSUE: Another issue we must address is the promise we made to preserve type-safety. We're supposed to guarantee that a given X_Id.Val type will be interchangeable with one and *only* one Ptr type (hopefully, type X.Ptr). If we can guarantee that valid X.Ptr values are the *only* things we'll stuff into X_Id.Val's, then we can guarantee that X.Ptr values will be the only things to come out of them again. But the existence of this Conversion generic seems to fly in the face of that. What if we try instantiating a given X_Id.Conversion generic more than once, for different Ptr types? Obviously, general type insanity would ensue. Clearly, to avoid breaking type safety, we must guarantee that a given X_Id.Conversion generic will be instantiated once and *only* once. But how do we prevent people from instantiating a given generic X_Id.Conversion more than once? One possibility is to just make it a point of software engineering discipline: Get a big mallet and pummel anyone who tries this. :-) Or perhaps we could introduce a special Once_Only pragma that can be applied to a generic template. Then we could hack up the compiler and the binder/linker to make it illegal to create an executable that contains more than one instantiation of a Once_Only generic. Maybe someone could experiment with this with the GNAT compiler. However, such a pragma would be compiler-specific, and thus not very portable. Another possibility is to simply add a run-time check that raises an exception if more than one instantiation of X_Id.Conversion is elaborated. This is pretty easy to do: ---------------------------------------------------------------------- package body Id is One_Conversion_Already : Boolean := False; Dual_Conversion_Error : exception; package body Conversion is function To_Id ... function To_Ptr ... begin -- Conversion if One_Conversion_Already then raise Dual_Conversion_Error; else One_Conversion_Already := True; end if; end Conversion; end Id; ---------------------------------------------------------------------- Unfortunately, this doesn't give us a mechanism for detecting this error at compilation time or bind/link time. We have to actually execute the program to detect the error. But at least this scheme will eventually detect this error. Moreover, it has the advantage of being 100% portable. And a bit of programmer discipline will generally keep this exception from cropping up at all. NOW FOR SOME MAGIC: At this point you may be chomping at the bit and saying, "This is all very nice, but how do we actually *implement* the Id.Val type and the Id.Conversion functions?" Well, all we really need is some type that can somehow "magically" hold the value of a general access-to-classwide-type, without loss of information. However, we need to establish this magical type long before we've actually declared any of our tagged types. One way to finesse this would be to simply hack up the compiler and make the whole Id generic an intrinsic (like Ada.Unchecked_Conversion, and so forth). I suppose somebody could experiement with the GNAT compiler to provide this capability. However, once again, this would not constitute a very portable solution. So in the meantime we need something we could use today, on any Ada9X compiler. I suggest implementing the Id.Conversion functions using Unchecked_Conversion. That's right, Unchecked_Conversion. Yes, it's controversial. However, one very nice feature is that, since Unchecked_Conversion is intrinsic, it is very likely that calls to our Id.Conversion functions would have *zero* run-time cost. I know what you're thinking: What about type safety? Doesn't Unchecked_Conversion throw all that out the window? Well, normally it would. But we just went to great pains to guarantee type-safety by assuring that there would be a strict one-to-one correspondence between a given X_Id.Val type and a given X.Ptr type. Also, remember that each X_Id.Val type is a private type, and so the repertoire of things we can do with it is severely limited: Basically, all we'll be able to do is flip back and forth between X_Id.Val's and X.Ptr's. So, all that remains is to make sure that an X_Id.Val can reliably hold the bit-pattern of an X.Ptr type (i.e., a general-access-to-classwide type) without any loss of information. Essentially, X_Id.Val could be any type at all, as long as X_Id.Val'Size = X.Ptr'Size. Probably the best way to assure this would be to implement X_Id.Val itself as a general-access-to-classwide type. The actual designated tagged type would not matter -- it could just be some "dummy" type, hidden in the private part, and never really used for anything. The only issue is portability. Will a given Ada compiler implement all general-access-to-classwide types with the same 'Size? I believe this is a reasonably safe assumption. However, we could very easily double-check this at elaboration time and raise an exception if we discover Id.Val'Size /= Ptr'Size. (At which point, we could perhaps try some other tricks.) Thus, the complete, fully bullet-proofed implementation of my generic Id package could be written as follows: ---------------------------------------------------------------------- generic package Id is type Val is private; None : constant Id.Val; generic type Obj (<>) is abstract tagged limited private; type Ptr is access all Obj'Class; package Conversion is function To_Id (The_Ptr : in Ptr) return Id.Val; function To_Ptr (The_Id : in Id.Val) return Ptr; end Conversion; private -- Id type Dummy is abstract tagged limited null record; -- never really used type Val is access all Dummy'Class; None : constant Id.Val := null; end Id; ---------------------------------------------------------------------- with Ada.Unchecked_Conversion; package body Id is One_Conversion_Already : Boolean := False; Dual_Conversion_Error : exception; Portability_Problem : exception; package body Conversion is function Ptr_To_Id is new Ada.Unchecked_Conversion (Ptr, Id.Val); function To_Id (The_Ptr : in Ptr) return Id.Val renames Ptr_To_Id; function Id_To_Ptr is new Ada.Unchecked_Conversion (Id.Val, Ptr); function To_Ptr (The_Id : in Id.Val) return Ptr renames Id_To_Ptr; begin -- Conversion if Id.Val'Size /= Ptr'Size then raise Portability_Problem; elsif One_Conversion_Already then raise Dual_Conversion_Error; else One_Conversion_Already := True; end if; end Conversion; end Id; ---------------------------------------------------------------------- All you need to do now is install this little support package into your library, and then start cranking out those mutually-coupled class packages! :-) (FYI: As of version 1.83, bugs in GNAT make it impossible to use this generic in this form, so you may need to wait a bit to try this. I'll give version 2.00 a shot before I file any bug reports, though.) Happy OOPing! :-) -- 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."); --------------------------------------------------------------------------------