======= LSN010.SimpOOP ======= !topic LSN on Simplifying the OOP proposals !reference MD-3.4*;2.0 !from Tucker Taft 91-06-05 !discussion We earlier sent out a list of potential OOP-related 9x proposals which were candidates for simplification or elimination. We have sent out a number of LSNs related to some of those OOP features, indicating their usefulness and place within the overall OOP model. This LSN will attempt to bring together the various threads of discussion, and make a concrete proposal for simplifying the set of 9X OOP features. ENUMERATION EXTENSION As discussed in a separate LSN, this capability is useful when an enumeration type has "interesting" class-wide operations (e.g. the boolean class can be used in "if" and "while" statements) which extend gracefully to the extension. It is also useful, when tagged, for building up incrementally a single global enumeration, though the resulting universal enumeration type is only partially ordered, and can't easily be used as a discrete type. Given the above issues with enumeration extension, it seems like a possible candidate for elimination as we seek a simpler OOP feature set. TAGGED ELEMENTARY TYPES As discussed in the same LSN with enumeration extension, tagged elementary types could be used to provide units on numeric types, but the solution does not seem significantly better than other alternatives using a normal discriminated record type (especially given the proposed Separate_Discriminants pragma -- see Chap 13 of MD2.0), or explicit declarations of the appropriate multiply and divide combinations. Furthermore, the rules for tagged elementary types are sufficiently different from tagged composite types that the added complexity may be harder to justify. Therefore, tagging on elementary types is another candidate for elimination. RECORD EXTENSION OF UNTAGGED TYPES Currently tagging and extension are independent concepts. A type may be tagged but never extended. Similarly a type may be the root for extensions even if not tagged. This seems to provide additional flexibility, but it increases the number of cases for compilers to worry about, and creates the somewhat uncomfortable situation where data is lost on conversion to the untagged universal type from an extension. An advantage of keeping the concepts independent is that any preexisting type can be extended without forethought, but the burden of only adding the "tagged" keyword to the root type is relatively mild. It does require recompilation, but should not require other changes to existing source code. Therefore, support for record extension of untagged types seems like another candidate for elimination. Note that from one perspective, disallowing extension of untagged types might be seen as an arbitrary restriction, and therefore a complication rather than a simplification. However, this is not really an arbitrary restriction given the fact that tagging is essential if the universal type is going to accomodate values from different extensions without data loss. By stressing this point in the presentation, the connection between tagging and support for extension should be more intuitive. WHAT'S LEFT? Given that we are now proposing that tagging and record extension be closely linked, it makes sense to go the other way and disallow tagging where record extension is not meaningful. Therefore, we are left with the following model: 1) Only record and private types may be tagged. The full type declaration for a tagged private type must be either a tagged record type declaration, or a derived type declaration where the parent is already tagged. I.e. tagging may not be added as part of a type derivation. Syntactically, the "tagged" keyword is only permitted immediately before "record" or "private" in a type declaration. The net effect is that all tagged types are records "down deep." 2) Only tagged record/private types may be extended with additional components, via either visible record extension, private record extension, or discriminant extension. Because only types which are internally record types can be tagged, then only record types can be extended. No "mongrels" can be created by extending a private type whose full type is not a record. Furthermore, we can drop the need to be able pass all private types by reference, since only internally record types can be tagged and extended. 3) All composite types may have discriminants. In the case of an array type the discriminants must be used to constrain the array. In the case of derived untagged types, the discriminants must be used to constrain the parent type (i.e. in the parent subtype indication). This implies that no additional space is ever needed for the new discriminants on an array type or derived untagged type -- they are always effectively "renames" (possibly with a constraint) of the array bound/parent discriminant. [NOTE: The Mapping Document section on Discriminants has a list of reasons why generalized discriminants are very useful -- please see that for more details.] 4) Eliminate the "with <>" option on generic formal derived types. Assume that the actual may be an extension if and only if the specified root type is tagged. I.e. "with <>" is implicit after "type NT is new T;" in a generic formal if T is tagged. 5) Allow a formal private type declaration to include the keyword "tagged"; the actual must be a tagged type in that case. Only tagged formal types may be extended. Tagged types may be passed to untagged formal types, but then extension is not permitted inside the generic. ================== We believe that this set of simplifications produces a very "lean and mean" OOP capability. There are very few options left, which should reduce the number of implementation cases to worry about (e.g. no need to support record extension on an arbitrary untagged private type). We have gained the comforting property that conversion to a universal type never loses data. We have eliminated the possibility for "mongrels" created by extending a non-record type. We have ensured that array and untagged derived type discriminants don't require additional storage. We have ensured that tags are always constant for the lifetime of a universal object, allowing OUT parameters to always be usable for controlling dispatching. DERIVING VS EXTENDING There are some interesting implications of making the strong connection between tagging (for dispatching) and extending. One is that we are effectively claiming that simple Ada-83-style derivation, where the derived type has the identical information content as the parent type, is not usually accompanied by lots of overriding of derivable operations. Instead, non-extending derived types are used for creating the somewhat "arbitrary" distinctions wanted when using numeric types to represent different physical units, or different array indexes, etc. We are trying to create barriers to prevent meaningless combinations of operands, but we are not trying to change the implementations of the operations. This claim that relatively little operation overriding happens with derived types is born out in our experience with Ada-83. Probably the one big exception to the rule is modular arithmetic types, but they may be the dreaded "exception that proves the rule." One might also say that this implies that derived types were a lot of mechanism for a relatively modest return. But as Paul H. says, they were a good "stub" for adding 9X OOP. Associated with this claim about little overriding upon simple deriving, is that substantial overriding will often be associated with extending. This will be especially true if the parent type is a null record, and its operations are all abstract! This implies that associating tagging/dispatching with extending does make sense, and is not simply a marriage of convenience. What this all means is that the semantics of untagged universal types, where the operations are simply those of the root type make sense because of the minimal overriding which happens in the absence of extension. In contrast, the dispatching semantics of tagged universal types also makes sense, since a class with multiple extensions of various shapes and sizes will necessarily have multiple implementations of the primitive operations. OOP AND GENERICS If we take this line of thought about the appropriateness of the non-dispatching semantics of untagged universal type operations, versus the dispatching semantics of tagged universal types into the realm of generics, an interesting thing happens. We suddenly have a consistent position if we *retain* the Ada 83 rule that the *predefined* operators reemerge for formal types in untagged classes. What we are claiming is that overriding predefined operators is a very rare event for untagged types, and so the implementation of universal types and generics can both *ignore* these events completely. Or approximately equivalently: The primitive operations of the universal type for a class are used inside a generic on a formal type of the class. This model should certainly allow the "universal" generic sharers to rest easier! In fact, we see that the set of "thunks" one has to create for an actual type passed into a shared generic are very similar to the descriptor pointed-to (identified-by) a type tag. For untagged types, this set of thunks should be very small or empty. For tagged types, the preexisting type descriptor is likely to be adequate. ========= Well, that should be enough head scratchers for one LSN... Comments, as usual, welcome. (Flames will be doused :-). -Tuck