[comp.lang.ada] OO Ada -- another way

pcg@cs.aber.ac.uk (Piercarlo Grandi) (11/05/90)

I have been wondering about supporting OO programming in Ada using
delegation and not inheritance. The following article follows some
musings of mine relative to C++.

Inheritance in OO language is about one bad and one clumsy thing:

* the bad thing is that inheritance as prefixing was introduced in
Simula 67 to make it possible to create composite objects using
contiguity and not just access types, and this was needed only because
Simula 67 would otherwise only have allowed object composition via
access types.

* the clumsy thing is that inheritance is a poor way to achieve reuse of
definition and reuse of implementation, because it ties the two together
in some gluey way, and one has to do funny things like abstract classes
to separate the two.

Delegation is another way of providing a cleaner algebra for doing the
same things that inheritance does; it may be better for Ada.

A particular form of delegation, which is equivalent to garden
inheritance, is the consequence of the following rule:

	allow any subfield of a record to be named with any unique
	subset of its full path name.

This effectively merges the definitions of the subrecords of a record,
while retaining some of their distinctiveness if needed. The same could
be allowed for packages.

Example:

	TYPE r1 IS RECORD a : natural; b : float; END RECORD;
	TYPE r2 IS RECORD c : char; d : sring(32); END RECORD;

	TYPE r3 IS RECORD s1 : r1; s2 : r2; END RECORD;

	s3 : r3;


We could then have:

	full paths	abbreviated paths

	s3.s1.a		s3.a
	s3.s1.b		s3.b
	s3.s2.c		s3.c
	s3.s2.d		s3.d

In other words, type 'r3' is the union of types 'r1' and 'r2'. A few Ada
specific syntax issues need a bit of thinking (how to merge parameter
lists, or package bodies), but they are far from insurmountable. So,
this takes care of what is commonly called 'inheritance'.


There is the other feature of OO programming, commonly called
'polymorphism', which is really runtime *definition* overloading, in the
sense that the same function definition can be applied to various
different types and the right implementation is chosen automatically at
runtime depending on the type of the arguments.

Ada function and operator overloading gives the compile time version of
polymorphism for procedures and functions. We need only a mechanism to
say that we want also run time overloading. Kind of bounded runtime
overloading is achieved with records with cases. We could actually
extend overloading (and genericity) to runtime types.

It's not difficult to think of a few different syntaxes for this, which
like the above proposal for merging the definitions of two records or
packages, would be totally backwards compatible.

For example one can currently overload statically area_of for squares
and circles like this:

	TYPE square IS RECORD side : float; END RECORD
	TYPE circle IS RECORD radius : float; END RECORD

	FUNCTION area_of(s : IN square) RETURN float
	IS BEGIN RETURN s.side * s.side; END area_of;

	FUNCTION area_of(r : IN radius) RETURN float
	IS BEGIN RETURN r.radius*r.radius*3.14159278; END area_of;

We want to express the notion that this should happen at run time;
currently we must write:

	TYPE which IS (asquare,acircle);

	TYPE figure(w : which) IS RECORD
	    CASE
	    WHEN asquare => s : square;
	    WHEN acircle => c : circle;
	    END CASE;
	END RECORD;

	FUNCTION area_of(f : IN figure) RETURN float;
	BEGIN
	    CASE f.w
	    WHEN asquare => RETURN area_of(f.s);
	    WHEN acircle => RETURN area_of(f.c);
	    END CASE;
        END area_of;

This is manually implemented bounded polymorphism. The 'manually' and
'bounded' are the objectionable parts. As an example of a possible
syntax we could have an unbounded definition like this:

	FUNCTION area_of(f : IN ACCESS) RETURN float; -- no IS allowed!

This would notify the compiler that 'area_of' is a runtime overloaded
function over any access type to those types for which a statically
overloaded area_of exists. This is akin to having a 'defgeneric' in
CLOS, and similar implementation techniques can be used.

When a polymorphic procedure is defined, the compiler could start to
collect in a table the addresses of its implementations, and in another
table the types on which these implementations are overloaded; making
the two tables available at run time would enable resolution of the
correct implementation.

For example one could have a table of pointers to implementations
indexed by a type code, and decorate each access value, or each accessed
value, with the relevant type code when a NEW is executed. Hash tables
indexed by both function code and type code could also be profitably
used, or even tables indexed by strings, depending on the depth of
compile and link time analysis desired, and on how many arguments are
dynamically overloaded.

One might use the unconstrained indicator <> to indicate unbounded
overloading, in yet another syntax form; for example to define less
unbounded types one could have:


	TYPE figure IS <>;

	FUNCTION area_of(f : figure) RETURN float;

	TYPE square IS RECORD f : figure; side : float; END RECORD
	TYPE circle IS RECORD f : figure; radius : float; END RECORD

In which the compiler has more compile time information; this looks very
much like abstract base classes in C++. The idea is that a type declared
as '<>' is really implemented as an access value to a tavle of pointers
to implementations, table to be indexed by the type code of the types
that contain a field like it.

----------------

Ahem: in case the discussion above raises eyebrows, I will repeat here
the gist of a previous posting of mine.

We want to be able to mix and match definitions with implementations
(and specifications, but Ada does not deal with them), and abstractions
over them, so that we can reuse them orthogonally. For example, we want
to be able to associate alternative implementations to a single
definition, or to use the same implementation with different
definitions, or to combine definitions or implementations with each
other; in general we want to define algebras over definitions,
implementations, specifications, so that arbitrary combinations and
abstractions are possible.

Examples: procedures are a way to abstract implementations over the
specific entities they operate upon; overloaded procedures are a way to
abstract procedure definitions over the type of arguments; type generic
procedures are a way to abstract procedure implementations over the type
of arguments. Functional languages have partial application, function
combination, that are other possibilities in the algebras of definitions
and of implementations respectively.

Type overloading is a property of a definition; it means providing
alternative definitions for the same procedure and letting the compiler
choose the right one based on the compile time type of the parameters.
Each definition has a statically associated implementation.

Type genericity is a property of an implementation; it means that the
implementation of a procedure is invariant with respect to a certain set
of types of the arguments. This is only possible if the implementation
only uses to manipulate those arguments operations overloaded on the
same set of types.

Both overloading and genericity can be either compile time or run time;
they are compile time if the type does not depend on the program's
inputs, run time if it does.

Polymorphism is runtime type overloading. It may be bounded, in which
case it is known at compile time that the type will belong to a certain
finite set, unbounded, when it will belong to certain infinite set (e.g.
it is know it is an access type, but not which), or unlimited, in which
nothing is known about the runtime characteristics of the type.

Runtime genericity is when all the operations on arguments in the
procedure implementation are polymorphic. It can also be classified as
bounded, unbounded or unlimited.

In Ada we have compile time overloading, compile time genericity; and a
bounded form of runtime overloading, with case records, in which the
types of the possible cases are known at compile time.

	What we need is some way to have unbounded or unlimited runtime
	overloading, and runtime genericity will be a consequence.

In Lisp we have bounded genericity, in that (approximately) the set of
types is not expandable, but any primitive works on any type (very
approximately).

In C++ we have compile time overloading and unbounded runtime
overloading (virtual functions are defined on all classes derived from
the same class), which may become unbounded runtime genericity if a
virtual function only calls other virtual functions.

In Smalltalk we have compile time overloading (two classes can define
the same method protocol) with run time overloading (the actual method
for a message send depends on both the protocol and the runtime type of
the receiver), and run time genericity (all message sends are run time
overloaded).
--
Piercarlo "Peter" Grandi           | ARPA: pcg%uk.ac.aber.cs@nsfnet-relay.ac.uk
Dept of CS, UCW Aberystwyth        | UUCP: ...!mcsun!ukc!aber-cs!pcg
Penglais, Aberystwyth SY23 3BZ, UK | INET: pcg@cs.aber.ac.uk