home *** CD-ROM | disk | FTP | other *** search
- Path: senator-bedfellow.mit.edu!bloom-beacon.mit.edu!newsfeed.internetmci.com!howland.reston.ans.net!surfnet.nl!swsbe6.switch.ch!swidir.switch.ch!epflnews!dinews.epfl.ch!Magnus.Kempe
- From: Magnus.Kempe@di.epfl.ch (Magnus Kempe)
- Newsgroups: comp.lang.ada,comp.answers,news.answers
- Subject: Ada FAQ: Programming with Ada (part 2 of 4)
- Followup-To: poster
- Date: 30 May 1996 17:08:50 GMT
- Organization: None
- Lines: 642
- Sender: magnus@lglsun4.epfl.ch (Magnus Kempe)
- Approved: news-answers-request@MIT.EDU
- Distribution: world
- Message-ID: <4okkn2$idt@disunms.epfl.ch>
- Reply-To: Magnus.Kempe@di.epfl.ch (Magnus Kempe)
- NNTP-Posting-Host: lglsun4.epfl.ch
- Mime-Version: 1.0
- Content-Type: text/plain; charset=iso-8859-1
- Content-Transfer-Encoding: 8bit
- Summary: Ada Programmer's Frequently Asked Questions (and answers),
- part 2 of 4.
- Please read before posting.
- Keywords: advanced language, artificial languages, computer software,
- data processing, programming languages, Ada
- Xref: senator-bedfellow.mit.edu comp.lang.ada:45674 comp.answers:18997 news.answers:73083
-
- Archive-name: computer-lang/Ada/programming/part2
- Comp-lang-ada-archive-name: programming/part2
- Posting-Frequency: monthly
- Last-modified: 22 May 1996
- Last-posted: 23 April 1996
-
- Ada Programmer's
- Frequently Asked Questions (FAQ)
-
- IMPORTANT NOTE: No FAQ can substitute for real teaching and
- documentation. There is an annotated list of Ada books in the
- companion comp.lang.ada FAQ.
-
- Recent changes to this FAQ are listed in the first section after the table
- of contents. This document is under explicit copyright.
-
- This is part 2 of a 4-part posting; part 1 contains the table of contents.
- Part 3 begins with question 6.
- Part 4 begins with question 9.
- Parts 3 and 4 should be the next postings in this thread.
- Part 1 should be the previous posting in this thread.
-
-
- 5: Object-Oriented Programming with Ada
-
-
- 5.1: Why does Ada have "tagged types" instead of classes?
-
- (Tucker Taft responds):
-
- Someone recently asked me to explain the difference between the
- meaning of the term "class" in C++ and its meaning in Ada 9X. Here is
- a synopsis of the answer:
-
- In C++, the term "class" refers to three different, but related
- things:
- * a language construct, that encapsulates the definitions of data
- members, member functions, nested types, etc.;
-
- * a particular kind of type, defined by a class construct (or by
- "struct" which is a special case of "class");
-
- * a set of types consisting of a type and all of its derivatives,
- direct and indirect.
-
-
- In Ada 9X, the term "class" refers only to the third of the above
- definitions. Ada 9X (and Ada 83) has three different terms for the
- concepts corresponding to the above three things:
- * a "package" encapsulates the definitions of types, objects,
- operations, exceptions, etc which are logically related. (The
- operations of a type defined immediately within the package where
- the type is declared are called, in 9X, the "primitive operations"
- of the type, and in some sense, define the "primitive" semantics
- of the type, especially if it is a private type.)
-
- * a "type" is characterized by a set of values and a set of
- primitive operations (there are a million definitions of "type,"
- unfortunately, but you know what I mean...);
-
- * a "class" is a set of types with similar values and operations; in
- particular, a type and and all of its derivatives, direct and
- indirect, represents a (derivation) class. Also, the set of
- integer types form the integer "class," and so on for the other
- language-defined classes of types in the language.
-
-
- Some OOP languages take an intermediary position. In CLOS, a "class"
- is not an encapsulating construct (CLOS has "packages"). However, a
- "class" is both a type and a set of types, depending on context.
- (Methods "float" freely.)
-
- The distinction Ada 9X makes between types and classes (= set of
- types) carries over into the semantic model, and allows some
- interesting capabilities not present in C++. In particular, in Ada 9X
- one can declare a "class-wide" object initialized by copy from a
- "class-wide" formal parameter, with the new object carrying over the
- underlying type of the actual parameter. For example:
-
- procedure Print_In_Bold (X : T'Class) is
- -- Copy X, make it bold face, and then print it.
- Copy_Of_X : T'Class := X;
- begin
- Make_Bold (Copy_Of_X);
- Print (Copy_Of_X);
- end P;
-
-
- In C++, when you declare an object, you must specify the "exact" class
- of the object -- it cannot be determined by the underlying class of
- the initializing value. Implementing the above procedure in a general
- way in C++ would be slightly more tedious.
-
- Similarly, in Ada 9X one can define an access type that designates
- only one specific type, or alternatively, one can define one that can
- designate objects of any type in a class (a "class-wide" access type).
- For example:
-
- type Fancy_Window_Ptr is access Fancy_Window;
- -- Only points at Fancy Windows -- no derivatives allowed
- type Any_Window_Ptr is access Window'Class;
- -- Points at Windows, and any derivatives thereof.
-
-
- In C++, all pointers/references are "class-wide" in this sense; you
- can't restrict them to point at only one "specific" type.
-
- In other words, C++ makes the distinction between "specific" and
- "class-wide" based on pointer/reference versus object/value, whereas
- in Ada 9X, this distinction is explicit, and corresponds to the
- distinction between "type" (one specific type) and "class" (set of
- types).
-
- The Ada 9X approach we believe (hope ;-) gives somewhat better control
- over static versus dynamic binding, and is less error prone since it
- is type-based, rather than being based on reference vs. value.
-
- In any case, in Ada 9X, C++, and CLOS it makes sense to talk about
- "class libraries," since a given library will generally consist of a
- set of interrelated types. In Ada 9X and CLOS, one could alternatively
- talk about a set of "reusable packages" and mean essentially the same
- thing.
-
-
- 5.2: Variant records seem like a dead feature now. When should I use them
- instead of tagged types?
-
- This is an instance of a much more general question: "When should I
- use what kind of type?" The simple answer is: "When it makes sense to
- do so." The real key to chosing a type in Ada is to look at the
- application, and pick the type that most closely models the problem.
-
- For instance, if you are modelling data transmission where the message
- packets may contain variable forms of data, a variant record --not a
- hierarchy of tagged types-- is an appropriate model, since there may
- be no relationship between the data items other than their being
- transmitted over one channel. If you choose to model the base type of
- the messages with a tagged type, that may present more problems than
- it solves when communicating across distinct architectures.
-
- [More to be said about variant programming vs. incremental
- programming.]
-
-
- 5.3: What is meant by "interface inheritance" and how does Ada support it?
-
- This answer intentionally left blank.
-
-
- 5.4: How do you do multiple inheritance in Ada 9X?
-
- There is a lengthy paper in file
- ftp://sw-eng.falls-church.va.us/public/AdaIC/flyers/9xm-inh.txt
-
- That document describes several mechanisms for achieving MI in Ada. It
- is not unusual, however, to find complaints about the syntax and the
- perceived burden it places on the developer. This is what Tucker Taft
- had to say when responging to such a criticism on comp.lang.ada:
-
- Coming up with a syntax for multiple inheritance was not the
- challenge. The challenge was coming up with a set of straightforward
- yet flexible rules for resolving the well known problems associated
- with multiple inheritance, namely:
- * If the same type appears as an ancestor more than once, should all
- or some of its data components be duplicated, or shared? If any
- are duplicated, how are they referenced unambiguously?
-
- * If the same-named (including same parameter/result profile)
- operation is inherited along two paths, how is the ambiguity
- resolved? Can you override each with different code? How do you
- refer to them later?
-
- * Etc.
-
-
- For answers, you can look at the various languages that define a
- built-in approach to multiple inheritance. Unfortunately, you will
- generally get a different answer for each language -- hardly a
- situation that suggests we will be able to craft an international
- consensus. Eiffel uses renaming and other techniques, which seem quite
- flexible, but at least in some examples, can be quite confusing (where
- you override "B" to change what "A" does in some distant ancestor).
- C++ has both non-virtual and virtual base clases, with a number of
- rules associated with each, and various limitations relating to
- downcasting and virtual base classes. CLOS uses simple name matching
- to control "slot" merging. Some languages require that all but one of
- the parent types be abstract, data-less types, so only interfaces are
- being inherited; however if the interfaces happen to collide, you
- still can end up with undesirable and potentially unresolvable
- collisions (where you really want different code for same-named
- interfaces inherited from different ancestors).
-
- One argument is that collisions are rare to begin with, so it doesn't
- make much different how they are resolved. That is probably true, but
- the argument doesn't work too well during an open language design
- process -- people get upset at the most unbelievably trivial and
- rarely used features if not "correctly" designed (speaking from
- experience here ;-).
-
- Furthermore, given that many of the predominant uses of MI (separation
- of interface inheritance from implementation inheritance, gaining
- convenient access to another class's features, has-a relationships
- being coded using MI for convenience, etc.) are already handled very
- well in Ada 9X, it is hard to justify getting into the MI language
- design fray at all. The basic inheritance model in Ada 9X is simple
- and elegant. Why clutter it up with a lot of relatively ad-hoc rules
- to handle one particular approach to MI? For the rare cases where MI
- is really critical, the last thing the programmer wants in the
- language is the "wrong" MI approach built in.
-
- So the basic answer is that at this point in the evolution of OO
- language design, it seemed wiser to provide MI building blocks, rather
- than to foist the wrong approach on the programmer, and be regretting
- it and working around it for years to come.
-
- Perhaps [Douglas Arndt] said it best...
-
- Final note: inheritance is overrated, especially MI. ...
-
-
- If the only or primary type composition mechanism in the language is
- based on inheritance, then by all means, load it up. But Ada 9X
- provides several efficient and flexible type composition mechanisms,
- and there is no need to overburden inheritance with unnecessary and
- complicated baggage.
-
-
- 5.5: Why are Controlled types so, well, strange?
-
- (Tucker Taft responds):
-
- We considered many approaches to user-defined finalization and
- user-defined assignment. Ada presents challenges that make it harder
- to define assignment than in other languages, because assignment is
- used implicitly in several operations (by-copy parameter passing,
- function return, aggregates, object initialization, initialized
- allocators, etc.), and because Ada has types whose set of components
- can be changed as a result of an assignment.
-
- For example:
-
- type T (D : Boolean := False) is record
- case D is
- when False => null;
- when True => H : In_Hands;
- end case;
- end record;
-
- X,Z : T;
- Y : T := (True, H => ...);
-
- ...
-
- X := Y; -- "X.H" component coming into existence
- Y := Z; -- "Y.H" component going out of existence
-
-
- With a type like the one above, there are components that can come and
- go as a result of assignment. The most obvious definition of
- assignment would be:
-
- procedure ":=" (Left : in out In_Hands; Right : in In_Hands);
-
-
- Unfortunately, this wouldn't work for the "H" component, because there
- is no preexisting "In_Hands" component to be assigned into in the
- first case, and in the second case, there is no "In_Hands" component
- to assign "from."
-
- Therefore, we decided to decompose the operation of assignment into
- separable pieces: finalization of the left hand side; simple copying
- of the data from the right hand side to the left hand side; and then
- adjustment of the new left hand side. Other decompositions are
- probably possible, but they generally suffer from not being easily
- composable, or not handling situations like the variant record above.
-
- Imagine a function named ":=" that returns a copy of its in parameter.
- To do anything interesting it will have to copy the in parameter into
- a local variable, and then "fiddle" with that local variable
- (essentially what "Adjust" does), and then return that local variable
- (which will make yet another copy). The returned result will have to
- be put back into the desired place (which might make yet another
- copy). For a large object, this might involve several extra copies.
-
- By having the user write just that part of the operation that
- "fiddles" with the result after making a copy, we allow the
- implementation to eliminate redundant copying. Furthermore, some
- user-defined representations might be position dependent. That is, the
- final "fiddling" has to take place on the object in its final
- location. For example, one might want the object to point to itself.
- If the implementation copies an object after the user code has
- adjusted it, such self-references will no longer point to the right
- place.
-
- So, as usual, once one gets into working out the details and all the
- interactions, the "obvious" proposal (such as a procedure ":=") no
- longer looks like the best answer, and the best answer one can find
- potentially looks "clumsy" (at least before you try to work out the
- details of the alternatives).
-
-
- 5.6: What do "covariance" and "contravariance" mean, and does Ada support
- either or both?
-
- (From Robert Martin) [This is C++ stuff, it should be completely
- re-written for Ada. --MK]
-
-
- R> covariance: "changes with"
- R> contravariance: "changes against"
-
- R> class A
- R> {
- R> public:
- R> A* f(A*); // method of class A, takes A argument and returns A
- R> A* g(A*); // same.
- R> };
-
- R> class B : public A // class B is a subclass of class A
- R> {
- R> public:
- R> B* f(B*); // method of class B overrides f and is covariant.
- R> A* g(A*); // method of class B overrides g and is contravariant.
- R> };
-
- R> The function f is covariant because the type of its return value and
- R> argument changes with the class it belongs to. The function g is
- R> contravariant because the types of its return value and arguments does not
- R> change with the class it belongs to.
-
-
- Actually, I would call g() invariant. If you look in Sather, (one of
- the principle languages with contravariance), you will see that the
- method in the decendent class actually can have arguments that are
- superclasses of the arguments of its parent. So for example:
-
- class A : public ROOT
- {
- public:
- A* f(A*); // method of class A, takes A argument and returns A
- A* g(A*); // same.
- };
-
- class B : public A // class B is a subclass of class A
- {
- public:
- B* f(B*); // method of class B overrides f and is covariant.
- ROOT* g(ROOT*); // method of class B overrides g and is contravariant.
- };
-
-
- To my knowledge the uses for contravariance are rare or nonexistent.
- (Anyone?). It just makes the rules easy for the compiler to type
- check. On the other hand, co-variance is extremely useful. Suppose you
- want to test for equality, or create a new object of the same type as
- the one in hand:
-
- class A
- {
- public:
- BOOLEAN equal(A*);
- A* create();
- }
-
- class B: public A
- {
- public:
- BOOLEAN equal(B*);
- B* create();
- }
-
-
- Here covariance is exactly what you want. Eiffel gives this to you,
- but the cost is giving up 100% compile time type safety. This seem
- necessary in cases like these.
-
- In fact, Eiffel gives you automatic ways to make a method covariant,
- called "anchored types". So you could declare, (in C++/eiffese):
-
- class A
- {
- public:
- BOOLEAN equal(like Current *);
- like Current * create();
- }
-
-
- Which says equal takes an argument the same type as the current
- object, and create returns an object of the same type as current. Now,
- there is not even any need to redeclare these in class B. Those
- transformations happen for free!
-
-
- 5.7: What is meant by upcasting/expanding and downcasting/narrowing?
-
- (Tucker Taft replies):
-
- Here is the symmetric case to illustrate upcasting and downcasting.
-
- type A is tagged ...; -- one parent type
-
- type B is tagged ...; -- another parent type
-
- ...
-
- type C; -- the new type, to be a mixture of A and B
-
- type AC (Obj : access C'Class) is
- new A
- with ...;
- -- an extension of A to be mixed into C
-
- type BC (Obj : access C'Class) is
- new B
- with ...;
- -- an extension of B to be mixed into C
-
- type C is
- tagged limited record
- A : AC (C'Access);
- B : BC (C'Access);
- ... -- other stuff if desired
- end record;
-
-
- We can now pass an object of type C to anything that takes an A or B
- as follows (this presumes that Foobar and QBert are primitives of A
- and B, respectively, so they are inherited; if not, then an explicit
- conversion (upcast) to A and B could be used to call the original
- Foobar and QBert).
-
- XC : C;
- ...
- Foobar (XC.A);
- QBert (XC.B);
-
-
- If we want to override what Foobar does, then we override Foobar on
- AC. If we want to override what QBert does, then we override QBert on
- BC.
-
- Note that there are no naming conflicts, since AC and BC are distinct
- types, so even if A and B have same-named components or operations, we
- can talk about them and/or override them individually using AC and BC.
-
-
- Upcasting (from C to A or C to B) is trivial -- A(XC.A) upcasts to A;
- B(XC.B) upcasts to B.
-
- Downcasting (narrowing) is also straightforward and safe. Presuming XA
- of type A'Class, and XB of type B'Class:
-
- AC(XA).Obj.all downcasts to C'Class (and verifies XA in AC'Class)
- BC(XB).Obj.all downcasts to C'Class (and verifies XB in BC'Class)
-
-
- You can check before the downcast to avoid a Constraint_Error:
-
- if XA not in AC'Class then -- appropriate complaint
-
- if XB not in BC'Class then -- ditto
-
-
- The approach is slightly simpler (though less symmetric) if we choose
- to make A the "primary" parent and B a "secondary" parent:
-
- type A is ...
- type B is ...
-
- type C;
-
- type BC (Obj : access C'Class) is
- new B
- with ...
-
- type C is
- new A
- with record
- B : BC (C'Access);
- ... -- other stuff if desired
- end record;
-
-
- Now C is a "normal" extension of A, and upcasting from C to A and
- (checked) downcasting from C'Class to A (or A'Class) is done with
- simple type conversions. The relationship between C and B is as above
- in the symmetric approach.
-
- Not surprisingly, using building blocks is more work than using a
- "builtin" approach for simple cases that happen to match the builtin
- approach, but having building blocks does ultimately mean more
- flexibility for the programmer -- there are many other structures that
- are possible in addition to the two illustrated above, using the
- access discriminant building block.
-
- For example, for mixins, each mixin "flavor" would have an access
- discriminant already:
-
- type Window is ... -- The basic "vanilla" window
-
- -- Various mixins
- type Win_Mixin_1 (W : access Window'Class) is ...
-
- type Win_Mixin_2 (W : access Window'Class) is ...
-
- type Win_Mixin_3 (W : access Window'Class) is ...
-
-
- Given the above vanilla window, plus any number of window mixins, one
- can construct a desired window by including as many mixins as wanted:
-
- type My_Window is
- new Window
- with record
- M1 : Win_Mixin_1 (My_Window'access);
- M3 : Win_Mixin_3 (My_Window'access);
- M11 : Win_Mixin_1(My_Window'access);
- ... -- plus additional stuff, as desired.
- end record;
-
-
- As illustrated above, you can incorporate the same "mixin" multiple
- times, with no naming conflicts. Every mixin can get access to the
- enclosing object. Operations of individual mixins can be overridden by
- creating an extension of the mixin first, overriding the operation in
- that, and then incorporating that tweaked mixin into the ultimate
- window.
-
- I hope the above helps better illustrate the use and flexibility of
- the Ada 9X type composition building blocks.
-
-
- 5.8: How does Ada do "narrowing"?
-
- Dave Griffith said
-
- . . . Nonetheless, The Ada9x committee chose a structure-based
- subtyping, with all of the problems that that is known to cause. As
- the problems of structure based subtyping usually manifest only in
- large projects maintained by large groups, this is _precisely_ the
- subtype paradigm that Ada9x should have avoided. Ada9x's model is,
- as Tucker Taft pointed out, quite easy to use for simple OO
- programming. There is, however, no good reason to _do_ simple OO
- programming. OO programmings gains click in somewhere around 10,000
- LOC, with greatest gains at over 100,000. At these sizes, "just
- declare it tagged" will result in unmaintainable messes. OO
- programming in the large rapidly gets difficult with structure based
- subtyping. Allowing by-value semantics for objects compounds these
- problems. All of this is known. All of this was, seemingly, ignored
- by Ada9x.
-
-
- (Tucker Taft answers)
-
- As explained in a previous note, Ada 9X supports the ability to hide
- the implementation heritage of a type, and only expose the desired
- interface heritage. So we are not stuck with strictly "structure-based
- subtyping." Secondly, by-reference semantics have many "well known"
- problems as well, and the designers of Modula-3 chose to, seemingly,
- ignore those ;-) ;-). Of course, in reality, neither set of language
- designers ignored either of these issues. Language design involves
- tradeoffs. You can complain we made the wrong tradeoff, but to
- continue to harp on the claim that we "ignored" things is silly. We
- studied every OOP language under the sun on which we could find any
- written or electronic material. We chose value-based semantics for
- what we believe are good reasons, based on reasonable tradeoffs.
-
- First of all, in the absence of an integrated garbage collector,
- by-reference semantics doesn't make much sense. Based on various
- tradeoffs, we decided against requiring an integrated garbage
- collector for Ada 9X.
-
- Secondly, many of the "known" problems with by-value semantics we
- avoided, by eliminating essentially all cases of "implicit
- truncation." One of the problems with the C++ version of "value
- semantics" is that on assignment and parameter passing, implicit
- truncation can take place mysteriously, meaning that a value that
- started its life representing one kind of thing gets truncated
- unintentionally so that it looks like a value of some ancestor type.
- This is largely because the name of a C++ class means differnt things
- depending on the context. When you declare an object, the name of the
- class determines the "exact class" of the object. The same thing
- applies to a by-value parameter. However, for references and pointers,
- the name of a class stands for that class and all of its derivatives.
- But since, in C++, a value of a subclass is always acceptable where a
- value of a given class is expected, you can get implicit truncation as
- part of assignment and by-value parameter passing. In Ada 9X, we avoid
- the implicit truncation because we support assignment for "class-wide"
- types, which never implicitly truncates, and one must do an explicit
- conversion to do an assignment that truncates. Parameter passing never
- implicitly truncates, even if an implicit conversion is performed as
- part of calling an inherited subprogram.
-
-
- 5.9: What is the difference between a class-wide access type and a "general"
- class-wide access type?
-
- What is exactly the difference between
-
- type A is access Object'Class;
-
- and
-
- type B is access all Object'Class;
-
- In the RM and Rationale only definitions like B are used. What's the
- use for A-like definitions ?
-
-
- (Tucker Taft answers)
-
- The only difference is that A is more restrictive, and so presumably
- might catch bugs that B would not. A is a "pool-specific" access type,
- and as such, you cannot convert values of other access types to it,
- nor can you use 'Access to create values of type A. Values of type A
- may only point into its "own" pool; that is only to objects created by
- allocators of type A. This means that unchecked-deallocation is
- somewhat safer when used with a pool-specific type like A.
-
- B is a "general" access type, and you can allocate in one storage
- pool, and then convert the access value to type B and store it into a
- variable of type B. Similarly, values of type B may point at objects
- declared "aliased."
-
- When using class-wide pointer types, type conversion is sometimes used
- for "narrowing." This would not in general be possible if you had left
- out the "all" in the declaration, as in the declaration of A. So, as a
- general rule, access-to-classwide types usually need to be general
- access types. However, there is no real harm in starting out with a
- pool-specific type, and then if you find you need to do a conversion
- or use 'Access, the compiler should notify you that you need to add
- the "all" in the declaration of the type. This way you get the added
- safety of using a pool-specific access type, until you decide
- explicitly that you need the flexibility of general access types.
-
- In some implementations, pool-specific access types might have a
- shorter representation, since they only need to be able to point at
- objects in a single storage pool. As we move toward 64-bit address
- spaces, this might be a significant issue. I could imagine that
- pool-specific access types might remain 32-bits in some
- implementations, while general access types would necessarily be
- 64-bits.
-