The variant records we studied in
Chapter
12 provide much expressive power to create complex types with several
different parts. However, they have an important limitation: A variant record
must be fully defined and compiled, and CASE
statements are used
to control processing its various parts.
Now suppose a new variant must be added. Consider the employee record of Section 12.4, which we repeat here for clarity:
TYPE PayCategories IS (Unknown, Professional, Sales, Clerical); TYPE Employee (PayStatus : PayCategories := Unknown) IS RECORD ID : IDType; 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;
Suppose
a new category of employee is added, Manager
, for example. The
enumeration type PayCategories
must be modified, and the variant
type declaration must be changed, adding a choice WHEN Manager
, to
account for the new variant. All operations on objects of the type must be
similarly changed.
Further, if the type declaration happened to appear in a package specification, every client of that package must at least be recompiled, and perhaps even modified. It would be nice if we could somehow extend a type, adding new fields and operations but without modifying or re-compiling existing packages or programs. This is called type extension and is provided in Ada by tagged types. A tagged type is analogous to a variant record, but it can be extended without changing the original type declaration.
TAGGED
to indicate that it will
potentially be extended by adding additional fields.[1] Each object of a tagged type is given a tag
by the compiler; you can think of a tag as analogous to a hidden discriminant.
Whereas with ordinary variant records the programmer must write explicit code
to use a discriminant, a tag is manipulated automatically in the executing
program.
As an example of a tagged type, consider representing a person with three general characteristics: a name, a gender, and a date of birth. We can declare this as:
TYPE Person IS TAGGED RECORD Name: NameType; Gender: Genders; BirthDate: Date; END RECORD;where
Genders
has been declared as:
TYPE Genders IS ( Female, Male);and the name and birth date fields are, respectively, some string type and a date from our package
Dates
.
Now suppose we declare Person
in the specification of a package
Persons
, together with a number of operations that are implemented
in the package body. We then write one or more client programs that use
Persons
. At a later date, we discover a need to represent
personnel, or persons working in a company. An employee is a person with a
company identifier and a second date indicating when she joined the company.
Note the "is a" relationship: An employee is a person with additional
characteristics. Without tagged types, we'd either develop an entire new
personnel type, or go back and modify our original person type. Using tagged
types, we can derive a new type based on the existing one:
TYPE Employee IS NEW Person WITH RECORD ID: IDRange; StartDate: Date; END RECORD;This declares a new type and reflects the "is a" relationship directly. Each employee now has five fields: the two new ones and the three it inherited from the person type. Furthermore, the new type can be declared in a new package, with a new set of operations, without disturbing the existing package or any programs that use it. This technique is called programming by extension.
We can carry this further, of course. The payroll department in our company wishes to extend our employee type for payroll purposes and so needs three special categories of employees. The new types can be derived from the employee type:
TYPE Professional IS NEW Employee WITH RECORD MonthSalary : Quantity; END RECORD; TYPE Sales IS NEW Employee WITH RECORD WeekSalary : Quantity; CommRate : CommissionPercentage; END RECORD; TYPE Clerical IS NEW Employee WITH RECORD HourlyWage : Quantity; END RECORD;where the
Quantity
values are taken from package Currency
.
In a further refinement of the "is a" relationship, a professional is an
employee, who in turn is a person. As before, the new types can be declared and
used in one or more new packages, without causing any modification of the older
packages or any of their clients.
It is instructive to note that in Ada 83 new types can be derived from ordinary Ada 83 types. The new type has the same structure (set of values) as the original, and the operations of the original type are generally inherited by the new one. Ada 95 adds to this the ability to extend the type.
Person Employee Professional Sales ClericalAda allows us to convert explicitly from a lower type to a higher one. If
P
is a Person
, E
is an
Employee
, and R
is a Professional
, we
can write an aggregate
R := ( Name => "Nancy", Gender => Female, BirthDate => Date_Of(1950, Oct, 21), ID => 2345, StartDate => Date_Of(1990, Jul, 1), MonthSalary => 5000.00);and can "up-convert" to
P
P := Person( R);which is a familiar conversion construct. In the case of tagged types, the conversion "strips off" the extra fields.
How do we "down-convert?" Since a conversion to a lower type generally adds fields, we use a special aggregate structure for this. If we had
P := (Name => "Nancy", Gender => Female, BirthDate => Date_Of(1950, Oct, 21);we could make
E
by writing
E := (P WITH ID => 2345, StartDate => Date_Of(1990, Jul, 1));The text following
WITH
is called an extension aggregate.
Generally, of course, client programs will not use the aggregate form because
types like these will, in general, be PRIVATE
. This brings us to
the subject of operations on tagged types.
The operations on tagged types are rather special. A fundamental Ada 95 notion
is the primitive operation. Put simply, a primitive operation of a type
is either a predefined operator on the type--like the operators on
Integer
, for example--or an operation (function, subprogram, or
operator) that is declared just below the type in the same package
specification and has a parameter of that type. Nearly all the operations in
the packages thus far in this book have been, in Ada 95 terminology, primitive.
The term becomes important in the context of tagged types. Each primitive
operation of a tagged type T is inherited by all types derived from T;
sometimes we desire the inheritance, but sometimes we do not.
We shall explain this in the context of three package specifications,
Persons
, Personnel
, and Payroll
, which
appear as
Program
15.1,
Program
15.2, and
Program
15.3 respectively.
Program 15.1Persons
WITH Dates; USE Dates; PACKAGE Persons IS ------------------------------------------------------------------------ --| Specification for Persons. This package provides a root type --| Person, with the fields Name, Gender, and BirthDate. Person --| is a tagged private type, which means that it has all the --| characteristics of an ordinary private type but also that it --| can be extended by derivation. --| Author: Michael B. Feldman, The George Washington University --| Last Modified: November 1995 ------------------------------------------------------------------------ TYPE Genders IS (Female, Male); SUBTYPE NameRange IS Positive RANGE 1..20; SUBTYPE NameType IS String(NameRange); TYPE Person IS TAGGED PRIVATE; -- selectors FUNCTION NameOf (Whom: Person) RETURN NameType; FUNCTION GenderOf(Whom: Person) RETURN Genders; FUNCTION DOBOf (Whom: Person) RETURN Date; -- Pre: Whom is defined -- Post: returns the appropriate field value PROCEDURE Put(Item: IN Person); -- Pre: Item is defined -- Post: Item's fields are displayed PACKAGE Constructors IS -- this inner package is necessary so that MakePerson is not a -- "primitive" function, that is, so that it is not inherited -- by types derived from Person. FUNCTION MakePerson(Name : String; Gender : Genders; BirthDate: Date) RETURN Person; -- Pre: Name, Gender, and BirthDate are defined -- Post: returns a Person with the given field values END Constructors; PRIVATE TYPE Person IS TAGGED RECORD NameLength: NameRange := 1; NameField : NameType := (OTHERS => ' '); Gender : Genders := Female; BirthDate : Date; END RECORD; END Persons;
In
Program
15.1, Person
is a PRIVATE
type with initialized
fields, as in most of our packages. Note, in the visible part of the
specification (above the PRIVATE
line), the declaration
TYPE Person IS TAGGED PRIVATE;which is consistent with our understanding of private declarations, with the addition of
TAGGED
. The package specification further gives four operations
in the selector category; this style is familiar to you from earlier packages.
However, the constructor operation is not declared here, but rather in an inner
package, Constructors
. Why the unfamiliar structure?
Our intention in writing Persons
is to allow new types to be
derived and extended from Person
. Consider the type
Employee
introduced earlier. An employee is a person with
additional fields; the type Employee
inherits all the primitive
operations of Person
, that is, for each primitive
Person
operation, there is a similar one for
Employee
, with a similar parameter profile. So the
Employee
type also has operations NameOf
,
GenderOf
, and DOBOf
.
Inheritance is fine for the selectors: For example, a client will certainly
wish to find out an employee's name, and an inherited operation just like the
Person
selector is a perfectly good operation to return the name.
The constructor is a different story, however, because we need to pass all the
field values into it. A person has three fields; an employee has five. If we
wrote a person constructor as a primitive operation, (e.g.,
MakePerson
), it would be inherited by the employee type, so a
client could call MakePerson
with a parameter of type
Employee
. But this would be wrong! The object would be constructed
with only three of its fields filled in!
Writing a separate constructor for Employee
is a useful thing
to do, and we shall do it shortly. However, it does not solve our problem
because MakePerson
would still be available for the client to call.
Because it would be very unsafe and therefore unwise to allow
MakePerson
to be inherited by derived types, we need to take
preventive action. There are several ways to do this; here we handle the
problem by realizing that--by Ada's rules of primitive operations--an operation
declared in an inner package, such as Persons.Constructors
in
Program
15.1, is not primitive and is therefore not inherited.
Putting the constructor in an inner package puts a small burden on the client
programmer, who can write Persons.NameOf
but must write
Persons.Constructors.MakePerson
. This is a small price to pay for
the added safety.
Program 15.2 gives the
specification for Personnel
.
Program 15.2
Personnel
WITH Persons; USE Persons; WITH Dates; USE Dates; PACKAGE Personnel IS ------------------------------------------------------------------------ --| Specification for Personnel, which provides a type Employee, --| a derivative of Persons.Person. Note that the operations on --| objects of type Persons.Person are inherited by objects of --| type Employee, so we need selectors only for the new --| fields! As in the case of Persons, we place the constructor --| in an inner package. --| Author: Michael B. Feldman, The George Washington University --| Last Modified: September 1995 ------------------------------------------------------------------------ TYPE Employee IS NEW Person WITH PRIVATE; -- Here is where Employee is derived; the extension fields are -- also PRIVATE, so clients cannot access them directly. TYPE IDType IS NEW Positive RANGE 1111..9999; -- selectors FUNCTION StartOf (Whom: Employee) RETURN Date; FUNCTION IDOf (Whom: Employee) RETURN IDType; -- Pre: Whom is defined -- Post: return the appropriate field values PROCEDURE Put(Item: Employee); -- Pre: Item is defined -- Post: Item is displayed PACKAGE Constructors IS -- as in Persons, we use an inner package to prevent the -- constructor from being inherited by further derivatives -- of Employee FUNCTION MakeEmployee(Name : String; Gender : Genders; BirthDate: Date; StartDate: Date; ID : IDType) RETURN Employee; -- Pre: Name, Gender, BirthDate, StateDate, and ID are defined -- Post: Whom contains the desired field values END Constructors; PRIVATE TYPE Employee IS NEW Person WITH RECORD ID : IDType := 1111; StartDate : Date; END RECORD; END Personnel;
Its
structure is similar to that of Persons
, but note how the type
Employee
is declared:
TYPE Employee IS NEW Person WITH PRIVATE;The syntax
WITH PRIVATE
indicates a private extension; it allows
Employee
to be a PRIVATE
type just as
Person
is. Personnel
also provides selectors
StartOf
and IDOf
, and a constructor
MakeEmployee
in an inner package.
The type Employee
inherits the primitive operations of
Person
: NameOf
, GenderOf
, and
DOBOf
. This is fine; employees also have these fields. What about
Put
? Persons.Put
displays the fields of a person. If
Put
were inherited by Employee
, it would, of course,
display only the fields that Employee
and
Person
have in common, which is not what we desire. We therefore
supply another Put
for the employee type. Because it has a similar
parameter profile, the only difference being the substitution of
Employee
for Person
, this new employee operation is
said to override the corresponding person operation. The body of
Personnel.Put
--we will show this shortly--displays all five fields
of an employee.
Why were we able to override Person.Put
so simply, without
using an inner package? The key is that the two Put
parameter
profiles are so similar. The constructors' parameter profiles are very
different from one another, so writing a MakePerson
in
Personnel
, with a profile appropriate for Employee
,
simply would not have solved that problem.
Program
15.3 gives the specification for Payroll
, which gives the
three pay categories we sketched earlier.
Program 15.3
Payroll
WITH Currency; USE Currency; WITH Dates; USE Dates; WITH Persons; USE Persons; WITH Personnel; USE Personnel; PACKAGE Payroll IS ------------------------------------------------------------------------ --| Specification for Payroll, a set of payroll categories --| derived from Personnel. Each type has a primitive operation --| Put, which overrides the one inherited from Employee. --| Author: Michael B. Feldman, The George Washington University --| Last Modified: September 1995 ------------------------------------------------------------------------ SUBTYPE CommissionPercentage IS Float RANGE 0.00..0.50; TYPE Professional IS NEW Employee WITH PRIVATE; TYPE Sales IS NEW Employee WITH PRIVATE; TYPE Clerical IS NEW Employee WITH PRIVATE; PROCEDURE Put(Item: Professional); PROCEDURE Put(Item: Sales); PROCEDURE Put(Item: Clerical); PACKAGE Constructors IS -- constructors for the three new types FUNCTION MakeProfessional(Name : String; Gender : Genders; BirthDate : Date; StartDate : Date; ID : IDType; MonthSalary: Quantity) RETURN Professional; FUNCTION MakeSales (Name : String; Gender : Genders; BirthDate : Date; StartDate : Date; ID : IDType; WeekSalary: Quantity; CommRate : CommissionPercentage) RETURN Sales; FUNCTION MakeClerical (Name : String; Gender : Genders; BirthDate : Date; StartDate : Date; ID : IDType; HourlyWage: Quantity) RETURN Clerical; -- Pre: All input fields are defined -- Post: Returns an initialized value of the respective type END Constructors; PRIVATE -- full extensions for the three types TYPE Professional IS NEW Employee WITH RECORD MonthSalary : Quantity; END RECORD; TYPE Sales IS NEW Employee WITH RECORD WeekSalary : Quantity; CommRate : CommissionPercentage; END RECORD; TYPE Clerical IS NEW Employee WITH RECORD HourlyWage : Quantity; END RECORD; END Payroll;The three types are closely related--all are used by the payroll department--so it is sensible to collect them into a single package as we have done here. Note the three derived
PRIVATE
type declarations, the three overriding
Put
operations, and the three constructors in the inner package.
We have not included field selectors, preferring to leave that as an exercise.
Before going on to the package bodies, look at Program 15.4, which illustrates the use of these packages.
Program 15.4
WITH Ada.Text_IO; USE Ada.Text_IO; WITH Currency; USE Currency; WITH Dates; USE Dates; WITH Persons; USE Persons; WITH Personnel; USE Personnel; WITH Payroll; USE Payroll; PROCEDURE Use_Payroll IS ------------------------------------------------------------------------ --| demonstrates the use of tagged types --| Author: Michael B. Feldman, The George Washington University --| Last Modified: September 1995 ------------------------------------------------------------------------ -- demonstrates the use of tagged types George: Person; Mary : Employee; Martha: Professional; Virginia: Sales; Herman: Clerical; BEGIN -- first construct all the people George := Persons.Constructors.MakePerson( Name => "George", Gender => Male, BirthDate => Date_Of(1971,Nov,2)); Mary := Personnel.Constructors.MakeEmployee( Name => "Mary", Gender => Female, BirthDate => Date_Of(1950,Oct,21), ID => 1234, StartDate => Date_Of(1989,Jul,1)); Martha := Payroll.Constructors.MakeProfessional( Name => "Martha", Gender => Female, BirthDate => Date_Of(1947,Jul,8), ID => 2222, StartDate => Date_Of(1985,Jun,6), MonthSalary => MakeCurrency(50000.00)); Virginia := Payroll.Constructors.MakeSales( Name => "Virginia", Gender => Female, BirthDate => Date_Of(1955,Feb,1), ID => 3456, StartDate => Date_Of(1990,Jan,1), WeekSalary => MakeCurrency(2500.00), CommRate => 0.25); Herman := Payroll.Constructors.MakeClerical( Name => "Herman", Gender => Male, BirthDate => Date_Of(1975,May,13), ID => 1557, StartDate => Date_Of(1991,Jul,1), HourlyWage => MakeCurrency(7.50)); -- Now display them all. Note that each Put is a different -- primitive operation. Put(Item => George); Ada.Text_IO.Put_Line(Item => "------------------------"); Put(Item => Mary); Ada.Text_IO.Put_Line(Item => "------------------------"); Put(Item => Martha); Ada.Text_IO.Put_Line(Item => "------------------------"); Put(Item => Virginia); Ada.Text_IO.Put_Line(Item => "------------------------"); Put(Item => Herman); Ada.Text_IO.Put_Line(Item => "------------------------"); END Use_Payroll;Sample Run
Name: George Gender: male Birth Date: November 2, 1971 ------------------------------ Name: Mary Gender: female Birth Date: October 21, 1950 ID Number: 1234 Start Date: July 1, 1989 ------------------------------ Name: Martha Gender: female Birth Date: July 8, 1947 ID Number: 2222 Start Date: June 6, 1985 Category: Professional Monthly Salary: 50000.00 ------------------------------ Name: Virginia Gender: female Birth Date: February 1, 1955 ID Number: 3456 Start Date: January 1, 1990 Category: Sales Weekly Salary: 2500.00 Commission Rate: 0.25 ------------------------------ Name: Herman Gender: male Birth Date: May 13, 1975 ID Number: 1557 Start Date: July 1, 1991 Category: Clerical Hourly Wage: 7.50 ------------------------------
Each
of the five variables is of a different type; in each case the appropriate
constructor is called--an Ada compiler would reject an attempt to call an
inappropriate one--and the appropriate Put
is used to display the
contents.
The bodies of Persons
, Personnel
, and
Payroll
are given as
Program
15.5,
Program
15.6, and
Program
15.7, respectively.
Program 15.5
Persons
WITH Ada.Text_IO; WITH Ada.Integer_Text_IO; WITH Dates.IO; PACKAGE BODY Persons IS ------------------------------------------------------------------------ --| Body of Persons package --| Author: Michael B. Feldman, The George Washington University --| Last Modified: September 1995 ------------------------------------------------------------------------ PACKAGE Gender_IO IS NEW Ada.Text_IO.Enumeration_IO(Enum => Genders); FUNCTION NameOf(Whom: Person) RETURN NameType IS BEGIN RETURN Whom.NameField; END NameOf; FUNCTION GenderOf(Whom: Person) RETURN Genders IS BEGIN RETURN Whom.Gender; END GenderOf; FUNCTION DOBOf(Whom: Person) RETURN Date IS BEGIN RETURN Whom.BirthDate; END DOBOf; PROCEDURE Put(Item: Person) IS BEGIN Ada.Text_IO.Put(Item => "Name: "); Ada.Text_IO.Put(Item => Item.NameField(1..Item.NameLength)); Ada.Text_IO.New_Line; Ada.Text_IO.Put(Item => "Gender: "); Gender_IO.Put(Item => Item.Gender, Set => Ada.Text_IO.Lower_Case); Ada.Text_IO.New_Line; Ada.Text_IO.Put(Item => "Birth Date: "); Dates.IO.Put(Item => Item.BirthDate, Format => Dates.IO.Full); Ada.Text_IO.New_Line; END Put; PACKAGE BODY Constructors IS FUNCTION MakePerson(Name : String; Gender : Genders; BirthDate: Date) RETURN Person IS Temp: NameType; BEGIN -- MakePerson Temp(1..Name'Length) := Name; -- copy into slice of Temp RETURN (NameLength => Name'Length, NameField => Temp, Gender => Gender, BirthDate => BirthDate); END MakePerson; END Constructors; END Persons;
Program 15.6
Personnel
WITH Ada.Text_IO; WITH Ada.Integer_Text_IO; WITH Dates.IO; PACKAGE BODY Personnel IS ------------------------------------------------------------------------ --| Body of Personnel package --| Author: Michael B. Feldman, The George Washington University --| Last Modified: September 1995 ------------------------------------------------------------------------ PACKAGE BODY Constructors IS FUNCTION MakeEmployee(Name : String; Gender : Genders; BirthDate: Date; StartDate: Date; ID : IDType) RETURN Employee IS BEGIN -- note how the Persons constructor is used, with an -- aggregate for the Person fields and an -- extension aggregate to add in the extra fields. RETURN (Persons.Constructors.MakePerson( Name => Name, Gender => Gender, BirthDate => BirthDate) WITH StartDate => StartDate, ID => ID); END MakeEmployee; END Constructors; FUNCTION StartOf (Whom: Employee) RETURN Date IS BEGIN RETURN Whom.StartDate; END StartOf; FUNCTION IDOf (Whom: Employee) RETURN IDType IS BEGIN RETURN Whom.ID; END IDOf; PROCEDURE Put(Item: Employee) IS BEGIN -- Note that we can convert Employee to Person and -- call Persons.Put for the common fields Persons.Put(Item => Persons.Person(Item)); Ada.Text_IO.Put(Item => "ID Number: "); Ada.Integer_Text_IO.Put(Item => Positive(Item.ID), Width => 1); Ada.Text_IO.New_Line; Ada.Text_IO.Put(Item => "Start Date: "); Dates.IO.Put(Item => Item.StartDate, Format => Dates.IO.Full); Ada.Text_IO.New_Line; END Put; END Personnel;
Program 15.7
Payroll
WITH Ada.Text_IO; WITH Ada.Float_Text_IO; WITH Currency.IO; PACKAGE BODY Payroll IS ------------------------------------------------------------------------ --| Body of Payroll package --| Author: Michael B. Feldman, The George Washington University --| Last Modified: September 1995 ------------------------------------------------------------------------ PACKAGE BODY Constructors IS -- constructors for the three new types FUNCTION MakeProfessional (Name : String; Gender : Genders; BirthDate : Date; StartDate : Date; ID : IDType; MonthSalary: Quantity) RETURN Professional IS BEGIN RETURN (Personnel.Constructors.MakeEmployee( Name => Name, Gender => Gender, BirthDate => Birthdate, StartDate => StartDate, ID => ID) WITH MonthSalary => MonthSalary); END MakeProfessional; FUNCTION MakeSales (Name : String; Gender : Genders; BirthDate : Date; StartDate : Date; ID : IDType; WeekSalary: Quantity; CommRate : CommissionPercentage) RETURN Sales IS BEGIN RETURN (Personnel.Constructors.MakeEmployee (Name => Name, Gender => Gender, BirthDate => Birthdate, StartDate => StartDate, ID => ID) WITH WeekSalary => WeekSalary, CommRate => CommRate); END MakeSales; FUNCTION MakeClerical (Name : String; Gender : Genders; BirthDate : Date; StartDate : Date; ID : IDType; HourlyWage: Quantity) RETURN Clerical IS BEGIN RETURN (Personnel.Constructors.MakeEmployee (Name => Name, Gender => Gender, BirthDate => Birthdate, StartDate => StartDate, ID => ID) WITH HourlyWage => HourlyWage); END MakeClerical; END Constructors; PROCEDURE Put(Item: Professional) IS BEGIN Put(Item => Employee(Item)); Ada.Text_IO.New_Line; Ada.Text_IO.Put(Item => "Category: Professional"); Ada.Text_IO.New_Line; Ada.Text_IO.Put(Item => "Monthly Salary: "); Currency.IO.Put(Item => Item.MonthSalary); Ada.Text_IO.New_Line; END Put; PROCEDURE Put(Item: Sales) IS BEGIN Put(Item => Employee(Item)); Ada.Text_IO.New_Line; Ada.Text_IO.Put(Item => "Category: Sales"); Ada.Text_IO.New_Line; Ada.Text_IO.Put(Item => "Weekly Salary: "); Currency.IO.Put(Item => Item.WeekSalary); Ada.Text_IO.New_Line; Ada.Text_IO.Put(Item => "Commission Rate: "); Ada.Float_Text_IO.Put(Item => Item.CommRate, Fore=>1,Aft=>2,Exp=>0); Ada.Text_IO.New_Line; END Put; PROCEDURE Put(Item: Clerical) IS BEGIN Put(Item => Employee(Item)); Ada.Text_IO.New_Line; Ada.Text_IO.Put(Item => "Category: Clerical"); Ada.Text_IO.New_Line; Ada.Text_IO.Put(Item => "Hourly Wage: "); Currency.IO.Put(Item => Item.HourlyWage); Ada.Text_IO.New_Line; END Put; END Payroll;Looking at Program 15.7, in the body of the constructor
MakeEmployee
we
up-convert the employee to a person, then use MakePerson
to fill
in the person fields. Finally, we use an extension aggregate to fill in the
remaining fields. Similarly, in the Put
procedure, we up-convert
as before and reuse the Persons.Put
to display the person fields,
and then we display the additional employee fields.
Copyright © 1996 by Addison-Wesley Publishing Company, Inc.