type DATE is record MONTH : MONTH_NAME; -- a suitable enumeration type DAY : INTEGER range 1 .. 31; YEAR : INTEGER range 1 .. 3000; end record;
Here the set of values consists of all ordered triples containing a month, a day, and a year in this order and having the specified component names MONTH, DAY, YEAR. The set of operations includes assignment, test for equality, component selection, and aggregate formation. For example, having declared
TODAY : DATE;
we can select the corresponding year by a selected component
as in the following assignment
TODAY.YEAR := TODAY.YEAR + 1;
Selection of the component YEAR can actually be viewed as achieved by a basic operation ".YEAR" which can be applied in postfix manner to the name of any object of type DATE. These basic operations are implicitly declared by the record type declaration itself, although Ada does not allow the explicit declaration of postfix operations such as ".YEAR".
Aggregates have already been discussed in section 3.5: in particular we have seen that Ada provides both positional aggregates and aggregates in named notation:
(DEC, 12, 1983) -- positional (DAY => 12, MONTH => DEC, YEAR => 1983) -- named
|In this section...|
type PAIR is record LEFT : INTEGER; RIGHT : INTEGER; end record; type RATIONAL is record NUMERATOR : INTEGER; DENOMINATOR : INTEGER range 1 .. INTEGER'LAST; end record;
Several alternative forms of structural equivalence rules can be considered, involving increasing amounts of checking, especially if the record types have a large number of components:
type ANOTHER_PAIR is record LEFT, RIGHT : INTEGER; end record;
Rule (c) makes sense for a language for which all aggregates are in positional notation. It complicates the checking by the compiler, since all permutations must be considered. Conversely, the rule (d) is sensible for a totally non-positional language where components must always be named in record aggregates. Rule (e) complicates the implementation of constraints and subtypes for components, since they must be checked for each component on record or array assignments. Rule (f) cannot be checked statically. Rule (g) requires a recursive matching algorithm. In addition, rule (h) requires type expansion, and even an algorithm of cycle reduction in the case of mutually recursive access types.
All these complexities for the implementation - and above all, for the reader - are avoided in Ada by adopting the simple rule that every declaration declares a distinct type.
Default initialization can be specified for some or for all components of a record. Consider for example:
type FRACTION is record DIVIDEND : INTEGER := 0; DIVISOR : POSITIVE := 1; end record;
The indicated initializations will be performed by the elaboration of the declaration of an object of type FRACTION, in the absence of explicit initialization. Thus after the elaboration of
F : FRACTION; G : FRACTION := (2, 3);
the value of F is well-defined and is equal to (0, 1), whereas the value of G is equal to (2, 3) as specified by the explicit initialization.
Note that initial values need not be static, as is illustrated here:
type BUFFER(LENGTH : POSITIVE) is record POS : NATURAL := 0; VALUE : STRING(1 .. LENGTH) := (1 .. LENGTH => ' '); end record; type TRIPLE is record A, B, C : PERSON_NAME := new PERSON; end record;
The following example shows that default initializations (in combination with access types) can even be used to construct quite elaborate dynamic structures:
type NODE(LEVEL : POSITIVE := 1); type LINK is access NODE; function BRANCH(N : POSITIVE) return LINK; type NODE(LEVEL : POSITIVE := 1) is record VALUE : ITEM := NULL_ITEM; LEFT, RIGHT : LINK := BRANCH(LEVEL); end record; function BRANCH(N : POSITIVE) return LINK is begin if N = 1 then return null; else return new NODE(N - 1); end if; end;
Thus whereas the declaration
TERMINAL : NODE(1);
will create a single node, a declaration such as
TREE : NODE(5);
will lead to the dynamic creation of a complete binary tree with 5 levels (thus including 1 node of level 5, 2 nodes of level 4, 4 nodes of level 3, 8 nodes of level 2, and 16 nodes of level 1).
The previous example is mainly intended to show the power that can be achieved by default initializations. Clearly more power also creates more danger and an incorrect program could certainly enter an infinite recursion during the elaboration of declarations.
The main motivation for allowing default initialization is however one of program reliability. In many applications, it is found desirable to have a consistent initial state for all objects: the services offered by the program may critically depend on objects being well initialized. To achieve this, we could of course define an initial value to be used for all declarations, or provide the users with an initialization procedure to be applied before any other use is made of objects. The weakness of these approaches lies in the fact that our program would remain vulnerable to users that do not follow this initialization discipline (whether unintentionally or not). The only safe solution is therefore to have a default initialization that is invoked without any reliance on the user.