Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Ada FAQ: Programming with Ada (part 2 of 4)

0 views
Skip to first unread message

Magnus Kempe

unread,
May 30, 1996, 3:00:00 AM5/30/96
to

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.

0 new messages