Previous | Next | Table of Contents | Index | Program List | Copyright

12.4 Data Structures: Variant Records

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 RECORD
indicates 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 IS
and 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
Four Variants of a Variant Record

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.00
In 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.

Displaying a Variant Record

The fragment in Figure 12.7 displays the data stored in the variant part of a record CurrentEmp. The value of CurrentEmp.PayStatus determines what information will be displayed.

Figure 12.7
Displaying a Variant Record

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

Form:
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;  

Example:
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; 

Interpretation:
The field-list for the fixed part is declared first. The variant part starts with the reserved word 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.

Note 1:
All field names must be unique. The same field name may not appear in the fixed and variant parts or in two field lists of the variant part.

Note 2:
An empty field list (no variant part for that CASE label) is indicated by NULL instead of a field list.

Note 3:
As in all 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.

Note 4:
If := 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.

Constrained and Unconstrained Variant Records

Ada has very strict rules to guarantee two things:

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:

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

We recommend that variant record type declarations usually have a default value supplied for the discriminant. Otherwise, all variables of that type will have to be constrained when they are declared, and much of the flexibility of variant records--especially their ability to change structure at execution time--will be lost.

Operations on Variant Records

As always in Ada, assignment and equality testing are defined for variant records. However, certain rules apply:

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.

Exercises for Section 12.4

Self-Check

  1. How many bytes of storage are required for each of the variants of Employee? You will probably have to check your Ada compiler documentation to determine the storage required by each of the fields comprising this record.

Programming

  1. Write a procedure to display a record of type Face as declared in the previous syntax display.


Previous | Next | Table of Contents | Index | Program List | Copyright

Copyright © 1996 by Addison-Wesley Publishing Company, Inc.