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 autolearn=no autolearn_force=no version=3.4.4 Xref: utzoo comp.lang.ada:4447 comp.object:2049 Path: utzoo!utgpu!news-server.csri.toronto.edu!rutgers!cs.utexas.edu!uunet!mcsun!ukc!dcl-cs!aber-cs!athene!pcg From: pcg@cs.aber.ac.uk (Piercarlo Grandi) Newsgroups: comp.lang.ada,comp.object Subject: OO Ada -- another way Message-ID: Date: 4 Nov 90 21:10:44 GMT Sender: pcg@aber-cs.UUCP Organization: Coleg Prifysgol Cymru List-Id: 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