comp.lang.ada
 help / color / mirror / Atom feed
From: jgv@swl.msd.ray.com (John Volan)
Subject: Re: cross linking packages
Date: Mon, 5 Dec 1994 13:30:58 GMT
Date: 1994-12-05T13:30:58+00:00	[thread overview]
Message-ID: <D0CAvn.Eut@swlvx2.msd.ray.com> (raw)
In-Reply-To: CzzAMz.K9F@inter.NL.net

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 :-)
\f
================================================================================

"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.");
--------------------------------------------------------------------------------




  parent reply	other threads:[~1994-12-05 13:30 UTC|newest]

Thread overview: 11+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
1994-11-28 12:56 cross linking packages Rene Burema
1994-11-29 18:28 ` Norman H. Cohen
1994-11-29 22:07 ` Robert Dewar
1994-11-30 16:33   ` David Weller
1994-12-01 13:44     ` Robert I. Eachus
1994-11-30 23:03   ` Matt Kennel
1994-12-02 14:30     ` Norman H. Cohen
1994-12-05 20:57     ` Elaboration order [was: cross linking packages] John Volan
1994-12-06 16:11       ` Robert Dewar
1994-12-05 13:30 ` John Volan [this message]
  -- strict thread matches above, loose matches on Subject: below --
1994-12-06 16:34 cross linking packages Bob Wells #402
replies disabled

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox