The record types we have seen so far are such that all records of a given record type have exactly the same form and structure. It is possible and often very useful, however, to define record types that have some fields that are the same for all variables of that type (fixed part) and some fields that may be different (variant part). Such a structure is called a variant record.
Consider an application from business information systems. There are three categories of employee in a particular company: One group (professionals) receives a fixed monthly salary, one group (sales) receives a fixed monthly salary plus a commission on their sales, and the third group (clerical) receives an hourly wage and is paid weekly based on number of hours worked.
How shall we represent a pay record for employees? The record type we saw in Section 10.5 is oversimplified; it does not take into account the different categories. We require a record type that can represent any of several structures, depending on the category. This is a perfect application for a variant record type.
A pay record for a given pay period has a fixed part giving the employee's ID and name, the ending date of the pay period, and a variant part giving the pay information according to the pay status. Given these basic type declarations:
SUBTYPE NameRange IS Positive RANGE 1..20; SUBTYPE NameType IS String(NameRange); SUBTYPE IDType IS Positive RANGE 1111..9999; SUBTYPE WorkHours IS Float RANGE 0.0..168.0; SUBTYPE CommissionPercentage IS Float RANGE 0.00..0.50; TYPE PayCategories IS (Unknown, Professional, Sales, Clerical);here is a declaration of this variant record type:
TYPE Employee ( PayStatus : PayCategories := Unknown) IS RECORD ID : IDType; NameLength: NameRange; Name : NameType; PayPeriod : Dates.Date; CASE PayStatus IS WHEN Professional => MonthSalary : Currency.Quantity; WHEN Sales => WeekSalary : Currency.Quantity; CommRate : CommissionPercentage; SalesAmount : Currency.Quantity; WHEN Clerical => HourlyWage : Currency.Quantity; HoursWorked : WorkHours; WHEN Unknown => NULL; END CASE; END RECORD;The line at the beginning of the record declaration,
TYPE Employee ( PayStatus : PayCategories := Unknown) IS RECORDindicates to the compiler that the record is a discriminated record which may have a variant part and that the discriminant field, which indicates which of several variants is present, is
PayStatus
. The discriminant is a
special field that looks like a parameter of a procedure; indeed, it has many
of the aspects of a parameter in that the record is parametrized, or
varies, according to the value of the discriminant. The reason for having a
value Unknown
used as a default will be explained shortly.
The fixed part of a record always precedes the variant part. The variant part
begins with the phrase
CASE PayStatus ISand declares the different forms the variant part can have. The
NULL
case indicates that there is no variant part for PayStatus
equal
to Unknown
. There are three different pay records, each of a
different variant.
For each variable of type PayRecord
, the compiler will usually
allocate sufficient storage space to accommodate the largest of the record
variants shown in
Figure
12.6. However, only one of the variants is defined at any given time;
this particular variant is determined by the discriminant field value.
Figure 12.6
Suppose we declare
Jane: Employee(PayStatus => Professional);Then Jane's record would look like the fixed part and variant 2 of the record in Figure 12.6. Because the value of
Jane.PayStatus
is
Professional
, only the variant field MonthSalary
may
be correctly referenced. All other variant fields are undefined. The program
fragment
Ada.Text_IO.Put("Jane's full name is "); Ada.Text_IO.Put(Jane.Name(1..Jane.NameLength)); Ada.Text_IO.New_Line; Ada.Text_IO.Put("and her monthly salary is $"); Ada.Float_Text_IO.Put(Jane.MonSalary, Fore => 1, Aft => 2, Exp => 0); Ada.Text_IO.New_Line;displays the lines
Jane's full name is Jane Smith and her monthly salary is $5000.00In Ada, the compiler and run-time system are very careful to check the consistency of the discriminant value with the references to fields in the record. If, at execution time, an attempt is made to access a field that is not defined in the current variant (i.e., the variant determined by the current discriminant value),
Constraint_Error
is raised. For this reason, a
CASE
statement is often used to process the variant part of a
record. By using the discriminant field as the CASE
selector, we
can ensure that only the currently defined variant is manipulated.
CurrentEmp
. The value of CurrentEmp.PayStatus
determines what information will be displayed.
Figure 12.7
Ada.Text_IO.Put(Item => "Employee ID ");Ada.Integer_Text_IO.Put(Item => CurrentEmp.ID, Width => 4);
Ada.Text_IO.New_Line; Ada.Text_IO.Put(Item => "Employee Name ");Ada.Text_IO.Put(Item => CurrentEmp.Name(1..CurrentEmp.NameLength));
Ada.Text_IO.New_Line; Ada.Text_IO.Put(Item => "Pay Period Ending ");Dates.Put(Item => CurrentEmp.PayPeriod, Format => Numeric);
Ada.Text_IO.New_Line;
CASE CurrentEmp.PayStatus IS
WHEN Unknown => Ada.Text_IO.Put(Item => "Unknown pay status!"); Ada.Text_IO.New_Line; WHEN Professional => Ada.Text_IO.Put("Monthly Salary is $"); Ada.Float_Text_IO.Put (Item=>CurrentEmp.MonthSalary, Fore=>1, Aft=>2,Exp=>0); Ada.Text_IO.New_Line; WHEN Sales => Ada.Text_IO.Put("Weekly Salary is $"); Ada.Float_Text_IO.Put (Item=>CurrentEmp.WeekSalary, Fore=>1, Aft=>2, Exp=>0); Ada.Text_IO.New_Line; Ada.Text_IO.Put("Commission percent is "); Ada.Float_Text_IO.Put (Item=>CurrentEmp.CommRate, Fore=>1, Aft=>2, Exp=>0); Ada.Text_IO.New_Line; Ada.Text_IO.Put("Sales this week $"); Ada.Float_Text_IO.Put (Item=>CurrentEmp.SalesAmount, Fore=>1, Aft=>2, Exp=>0); Ada.Text_IO.New_Line; WHEN Clerical => Ada.Text_IO.Put("Hourly wage is $"); Ada.Float_Text_IO.Put (Item=>CurrentEmp.HourlyWage, Fore=>1, Aft=>2, Exp=>0); Ada.Text_IO.New_Line; Ada.Text_IO.Put("Hours worked this week "); Ada.Float_Text_IO.Put (Item=>CurrentEmp.HoursWorked, Fore=>1, Aft=>2,Exp=>0); Ada.Text_IO.New_Line END CASE;
SYNTAX DISPLAY
Record Type with Variant Part
TYPE rec-type (discriminant : disc_type := default) IS RECORD ID1 : type1; ID2 : type2; . . fixed part . IDn : typen; CASE discriminant IS WHEN value1 => field-list1 WHEN value2 => field-list2 . . variant part . WHEN valuen => field-listn; WHEN OTHERS => others-field-list END CASE; END RECORD;
TYPE Face (Bald : Boolean) IS RECORD Eyes : Color; Height: Inches; CASE Bald IS WHEN True => WearsWig : Boolean; WHEN False => HairColor : Color; END CASE; END RECORD;
CASE
. The identifer discriminant is the name of the
discriminant field of the record; the discriminant field name is separated by a
colon from its type (disc-type), which must be type
Boolean
, an enumeration type, or a subrange of a discrete type.
The CASE
values (value1, value2 ,...,
valuek) are lists of values of the discriminant field as
defined by
discriminant-type. Field-listi describes the record
fields
associated with valuei. Each element of field-listi specifies a
field name and its type.
CASE
label) is indicated by NULL
instead of a field
list.
CASE
forms, all values of the
discriminant must be covered by WHEN
clauses. Values not covered
otherwise can be covered by a WHEN OTHERS
clause.
:=
default is omitted from the
discriminant declaration, all variables of the type must be constrained at the
time they are declared; that is, a value for the discriminant must be supplied.
If the default is present, unconstrained variables may be declared; that is,
variables without an explicit discriminant value. The first condition is ensured by requiring that if a default value for the
discriminant is not present in the record declaration, all
declarations of variables must supply a value for the discriminant. In the pay
status case above, a default of Unknown
is supplied; therefore it
is possible to declare a record without a discriminant value, as in
CurrentEmp : PayRecord;
Supplying a discriminant value is not prohibited, however;
AnotherEmp : PayRecord(PayStatus=>Professional);is allowed. In the case of the
Face
record above,
it would be a compilation error to declare
JohnsFace : Face;and in this case a discriminant value is required:
JohnsFace : Face(Bald=>False);
An unconstrained record variable is one that has a
default discriminant value and none is supplied in the variable declaration. It
is permissible to change the discriminant value of an unconstrained record at
execution time, under rules to be specified in the next section. This means
that the variable CurrentEmp
can hold a professional employee at
one moment, a sales employee at another. This is a common use of variant
records in data processing.
A constrained record variable is one whose discriminant value is
supplied when the variable is declared. Both AnotherEmp
and the
second JohnsFace
are constrained. It is not permitted to
change the discriminant value of a constrained record at execution time; this
means that we are "stuck" with the discriminant value. AnotherEmp
is constrained because we chose to make it so even though the discriminant has
a default; JohnsFace
is constrained because we have no choice,
because no default is supplied for Bald
. JohnsFace
cannot take into account his losing his hair at a later date.
Storing Values into Variant Records
Ada's rules for variant records may seem cumbersome, but the rules are designed to guarantee that the contents of a variant record are always consistent. Here are the basic rules for storing values into a variant record variable:
Constraint_Error
is raised.
A common application of variant records is to read the value of a discriminant from the terminal or a file, then create a record variable with that variant. By the rules above, the value cannot be stored directly into the discriminant. It, and the other fields of the record, must be held in temporary variables and stored as a unit into the variant record using an aggregate.
As we have seen, there is often a distinct advantage in supplying a default value for the discriminant. If we do not, all variables of the type must be constrained when they are declared, and much of the flexibility of variant records--especially their ability to change structure at execution time--is lost.
PROGRAM STYLE
Declaring Variant Records
As always in Ada, assignment and equality testing are defined for variant records. However, certain rules apply:
Constraint_Error
is
raised.
Section 10.5 developed an ADT for handling a data base of employee records. As an exercise, you can modify that ADT, and the associated interactive client program, to handle the more realistic variant employee records described in this section.
Employee
? You will probably have to check your Ada compiler
documentation to determine the storage required by each of the fields
comprising this record.
Face
as
declared in the previous syntax display.
Copyright © 1996 by Addison-Wesley Publishing Company, Inc.