11.7 The Variant Record--a Chameleon

Not only is it possible to model the data in a complex situation using a record that contains a variety of types and structures of data, it is even possible to arrange things so that the structure of the record is not itself fixed, but varies from one situation to another.

Depending on the value contained in a designated fields, the ones that follow can have different identifiers and there may even be a different number of them. Suppose one wanted the record person to contain the year and identity number only if the data represented a student, but the rank and pay if it was a faculty member, and the name of the position if it was a staff member. The declarations might then look something like this:

TYPE
  Name = ARRAY [0 .. 20] OF CHAR;
  Classification = (student, faculty, staff);
  Year = (freshman, sophomore, junior, senior);
  Rank = (instructor, assistant, associate, professor);
  Job = (secretary, maintenance, janitor);
  Date =
    RECORD
      year : CARDINAL;
      month : MonthType;
      day : [1 .. 31];
    END;   (* of the record Date *)
  Person =
    RECORD
      lastname, firstname : Name;
      birthdate : Date;
      (* use previous declarations for these. *)
      male : BOOLEAN;
      CASE status : Classification OF  (* varient part here *)
        student:
          idnumber : CARDINAL;
          year : CARDINAL |
        faculty:
          position : Rank;
          pay : REAL |
        staff:
          occupation : Job;
      END;  (* case *)
      married : BOOLEAN;
    END;  (* of the record Person *)

In an arrangement like this, the contents of the field status will determine the following fields and their names. The selection is outlined using the CASE construction. This is somewhat similar to the CASE statement, except that each value that is a potential selection is followed not by a statement sequence, but by the appropriate field names and their types for that particular case.

A record field whose sole purpose is to select the possible fields to follow is called a discriminator or a tag field or a selector field.

NOTES: 1. This particular record has seven fields if the status field contains the value student or the value faculty and only six if it has the value staff. The names of the other fields change accordingly.

2. Any number of variant sections can be introduced in a record and these can be intermingled freely with the fixed fields. Note that in this respect, Modula-2 differs from some other languages which restrict the number of variant sections to one and require it to be last.

However, some non-standard implementations may otherwise restrict this by requiring that all but the last of such variant fields take the same amount of memory for each possible value of the tag that determines that variant. In such versions, only the variant field that comes last in the record can have differing amounts of memory consumed by the various cases.

3. Each CASE section of the declaration has its own END.

4. The syntax of the variant part of a record is similar to that of the CASE statement. For instance, the cases can be selected by lists or ranges of several values.

5. An ELSE clause is allowed, and must be present if not all the values of the tag field have been listed in the various cases.

6. Repeated or redundant case separators (such as before the ELSE) are permitted in ISO and all but the oldest of other versions of Modula-2, just as in the CASE statement.

The general syntax of the variant part is:

CASE tagName : tagType OF
  selector list a:
    fieldname 1a : fieldtype 1a;
    fieldname 2a : fieldtype 2a;
    ...

    fieldname na : fieldtype na |
  selector list b:
    ...
                |
  selector list z
    ...
                |
    fieldname nz : fieldtype nz
  ELSE
    last list of fields
  END;

NOTE: If one of the selectors will have no fields associated with it, none are put after the colon following the selector label, but even in such cases, the case separator between cases cannot be left out.

The requirement on the else clause means that the following is illegal:

TYPE
  Range = [0..10];
VAR
  dist : Range;
TYPE
  Variant =
    RECORD
      CASE dist OF
        2,3,4..7 :
          int : INTEGER; |
        1 :
          card : CARDINAL
        END;  (* illegal *)

because the case values 6,8,9, and 10 have not been covered. It could be replaced by one of the following:

TYPE
  Variant =
    RECORD
      CASE dist OF
        2,3,4..7 :
          int : INTEGER; |
        1 :
          card : CARDINAL |
        6, 8..10 :
        END;

or by:

TYPE
  Variant =
    RECORD
      CASE dist OF
        2,3,4..7 :
          int : INTEGER; |
        1 :
          card : CARDINAL
        ELSE
        END;

In some situations the tag field is anonymous--it has a type, but no name. If so, then the colon and the field name type are still required.

A variant part of a record that has no named tag or discriminator field is called an undiscriminated union.
TYPE
  Number =
    RECORD
       CASE : BOOLEAN OF (* undiscriminated *)
       (* no field name, but colon and type needed *)
         TRUE:
            int: INTEGER
       |  FALSE:
            card : CARDINAL
         END
      END;
VAR
  num: Number;

In this last situation, there is only one field, because the tag field has not been named. The one field that is present can, of course, be referred to either as num.int, or as num.card. In the former case, it is an integer, and in the latter, it is a cardinal. This ability could sometimes be quite important in Pascal, which lacked any other means of coercion to reinterpret the contents of a variable of one type as being of another type, but it is less likely to serve the same role in Modula-2, and has been commented on here only for the sake of completeness. In Modula-2 one would use:

int := SYSTEM.CAST (INTEGER, card);

rather than sometimes refer to the item as a cardinal and others as an integer. This latter solution is only possible if both types occupy the same amount of storage space. The undiscriminated union would still work, but may give very strange results if both do not take the same amount of space.

In the discriminated case (tag field has a name) it is the programmer's responsibility to ensure that the correct field name is chosen for filling in the record in a given situation. For instance, for the type Person defined earlier, one could declare:

CONST
  nullStudent = Person {"", {1900, 1, 1}, TRUE, student, 0, freshman}

as a default record for initializing variables prior to entering student data. Note that the value of the tag field must be given in the constructor along with values for the variant fields that value determines are needed.

It is now possible to give the complete syntax diagram for the record declaration (figure 11.4):

Naturally, a CASE statement is convenient when it comes time to write the code to fill the fields of a variant record.

PROCEDURE Fill (VAR person : Person);
VAR
  ch : CHAR;
   index : CARDINAL;
BEGIN
  WITH person
    DO
      WriteString ("Last name, please. ");
      ReadString (lastname); SkipLine;
      WriteLn;
      WriteString ("And now the first name: ");
      ReadString (firstname); SkipLine;
      WriteLn;
      (* code to read date can be placed here *)
      WriteString ("Is this a male?  Answer Y or N. ");
      ReadChar (ch); SkipLine;
      male := (CAP (ch) = "Y");
      WriteString ("Enter a '0' for a student ");
      WriteLn;
      WriteString ("a '1' for a faculty member, ");
      WriteLn;
      WriteString ("or a '2' for a staff member. ");
      ReadCard (index); SkipLine;
      WriteLn;
      Status := VAL (Classification, index);

      CASE Status OF
        student:
          WriteString ("Give i.d. number, please. ");
          ReadCard (idnumber); SkipLine;
          WriteLn;
          WriteString ("and enter the year 1 .. 4 ");
          WriteString ("of studies. ");
          ReadCard (index); SkipLine;
          year := VAL (Year, index - 1) |
        faculty:
          WriteString ("Enter the rank of the faculty member ");
          WriteLn;
          WriteString ("by number.  A '1' for instructor, ");
          WriteLn;
          WriteString ("a '2' for assistant, a '3' for associate, ");
          WriteLn;
          WriteString ("or a '4' for a full professor. ");
          ReadCard (index); SkipLine;
          position := VAL (Rank, index - 1);
          WriteLn;
          WriteString ("How much is this faculty member paid? ");
          WriteLn;
          WriteString ("Answer using decimal point, please. ");
          GetNum (pay);  (* remember to include this procedure *) |
        staff:
          WriteString ("Please enter a '1' for a secretary, ");
          WriteLn;
          WriteString ("a '2' for a maintenance employee, ");
          WriteLn;
          WriteString ("or a '3' for a janitor. ");
          ReadCard (index); SkipLine;
          WriteLn;
          occupation := VAL (Job, index - 1);  (* no bar here *)
      END;  (* CASE *)

      WriteString ("Is this person married?  Y / N  ");
      ReadChar (ch); SkipLine;
      married := ( CAP (ch) = 'Y');
    END;  (* WITH *)
END Fill;

It would probably be wise to do a little idiot-proofing on this sort of data entry. There are several places where a person is asked to enter a CARDINAL in a certain range. (0 .. 2, 1 .. 4, 1 .. 3) Should the entry be incorrect, a fatal error and an inelegant exit to the outer system is likely to take place. The following procedure is somewhat more foolproof:

PROCEDURE GetCard (min, max : CARDINAL) : CARDINAL;
VAR
  number : CARDINAL;
BEGIN

  REPEAT
    WriteLn;
    WriteString ("Enter a number in the range ");
    WriteCard (min, 10);
    WriteString (" to ");
    WriteCard (max, 10);
    WriteString (" here ==> ");
    ReadCard (number); SkipLine;
  UNTIL (ReadResult () = allRight) AND (number >= min) AND (number <= max);
  RETURN number;
END GetCard;

In the main program, this procedure can be called by:

year := ORD (Year, GetCard (1, 4) - 1)

and suitable modifications would need to be made to other portions of the first procedure as well.

The variant record is an extraordinarily versatile construction. It allows for a free-form structure to data that incorporates all the possible variations that the data can have, while maintaining certain common features among related structures. (Every person has a last name, a first name, etc.)


Contents