Download Procedures and Functions in Ada 95: Exchange, Maximum, and Generic Binary Search and more Papers Computer Science in PDF only on Docsity!
from M.B. Feldman, Software Construction and Data
Structures with Ada 95, Addison Wesley, 1996.
CHAPTER 5
Generic Subprograms and Packages
5.1 Ada Structures: Generic Units
5.2 Application: A Generic Sorting Program
5.3 Application: a Generic Binary Search Program
5.4 ADT Design: An Abstract Data Type for Sets
5.5 Application: Music Makers
5.6 ADT Design: A Generic Vector Package
5.7 Summary of Generic Specifications
5.8 ADT Design: Generic Keyed Table Handler
5.9 ADT Design: A Generic Backup Package
5.10 Application: Airline Passenger List
5.11 ADT Design: ADTs versus Abstract Data Objects
This chapter introduces you to an important feature of Ada that makes the language extremely useful for
developing reusable software components: generics. A generic component (package or subprogram) is one
that is parametrized at the level of the types it works with. There are generic formal and actual parameters,
just like the “normal” ones we use with subprograms and variant records. A generic component can be
instantiated or “tailored” to work with a specific type. This means that a very general program or package
can be written whose code is independent of the type it manipulates. Versions of it can be created, using a
single statement for each version, to handle many different types.
This chapter shows you how to create your own generics and tailor them for many interesting
purposes; the remaining chapters of this book introduce many other generic units. Through the careful
design of generic units, an entire industry of reusable, tailorable software components can be built up and
used for a wide range of applications.
5.1 Ada Structures: Generic Units
Ada’s system of types and procedures requires that the type of a procedure’s actual parameter always match
that of the formal parameter. This means that a procedure or function that needs to do the same thing to
values of two different types must be written twice—once for each type. Consider the procedure
Exchange:
PROCEDURE Exchange(Value1, Value2: IN OUT Natural) IS TempValue: Natural; BEGIN TempValue := Value1; Value1 := Value2; Value2 := TempValue; END Exchange;
A procedure to exchange two Float values would have the same sequence of statements, but the type
references would be different:
PROCEDURE Exchange(Value1, Value2: IN OUT Float) IS TempValue: Float; BEGIN TempValue := Value1; Value1 := Value2; Value2 := TempValue; END Exchange;
Obviously, we could modify the first version to give the second version by using an editor. Because we
are likely to need the Natural version again, we modify a copy of it. This gives two versions of a
procedure that are almost the same; because of overloading, the two can both be called Exchange.
Carrying this to its extreme, we could build up a large library of Exchange programs with our editor and
be ready for any eventuality. Exchange could even be made to work with array or record structures,
because Ada allows assignment for any type.
There is a problem with this approach: It clutters our file system with a large number of similar
programs. Worse still, suppose that a bug turns up in the statements for Exchange or in another program
with more complexity. The bug will have turned up in one of the versions; the same bug will probably be
present in all of them, but we will probably forget to fix all the others! This is, in miniature, a problem
long faced by industry: multiple versions of a program, all similar but not exactly alike, all requiring
debugging and other maintenance.
Returning to our simple example, it would be nice if we could create one version of Exchange, test
it, and then put it in the library. When we needed a version to work with a particular type, we could just tell
the compiler to use our pretested Exchange but to change the type it accepts. The compiler would make
the change automatically, and we would still have only a single copy of the procedure to maintain.
It happens that Ada allows us to do exactly this. The solution to this problem is generics. A generic
unit is a recipe, or template, for a procedure, function, or package. Such a unit is declared with formal
parameters that are types, and sometimes are procedure or function names. An analogy can be drawn with an
unusual recipe for a layer cake: All the elements are there except that the following items are left as
variables to be plugged in by the baker:
- The number of layers
- The kind of filling between the layers
- The flavor of the cake itself
- The flavor of the icing
This recipe was pretested by the cookbook author, but before we can use it for a three-layer yellow cake
with marshmallow filling and chocolate icing, we need to (at least mentally) make all the changes necessary
to the list of ingredients. Only after this instance of the recipe has been created does it make sense to try to
make a cake using it.
Program 5.
A Test of the Generic Swap Procedure
WITH Swap_Generic; WITH Ada.Text_IO; WITH Ada.Integer_Text_IO; PROCEDURE Test_Swap_Generic IS
--| Test program for Swap_Generic --| Author: Michael B. Feldman, The George Washington University --| Last Modified: September 1995
X : Integer; Y : Integer;
A : Character; B : Character;
PROCEDURE IntegerSwap IS NEW Swap_Generic (ValueType => Integer); PROCEDURE CharSwap IS NEW Swap_Generic (ValueType => Character);
BEGIN -- Test_Swap_Generic
X := 3; Y := -5; A := 'x'; B := 'q';
Ada.Text_IO.Put("Before swapping, X and Y are, respectively "); Ada.Integer_Text_IO.Put(Item => X, Width => 4); Ada.Integer_Text_IO.Put(Item => Y, Width => 4); Ada.Text_IO.New_Line;
IntegerSwap(Value1 => X,Value2 => Y);
Ada.Text_IO.Put("After swapping, X and Y are, respectively "); Ada.Integer_Text_IO.Put(Item => X, Width => 4); Ada.Integer_Text_IO.Put(Item => Y, Width => 4); Ada.Text_IO.New_Line; Ada.Text_IO.New_Line;
Ada.Text_IO.Put("Before swapping, A and B are, respectively "); Ada.Text_IO.Put(Item => A); Ada.Text_IO.Put(Item => B); Ada.Text_IO.New_Line;
CharSwap(Value1 => A,Value2 => B);
Ada.Text_IO.Put("After swapping, A and B are, respectively "); Ada.Text_IO.Put(Item => A); Ada.Text_IO.Put(Item => B); Ada.Text_IO.New_Line;
END Test_Swap_Generic;
Generic Subprogram Parameters
Sometimes a generic recipe needs to be instantiated with the names of functions or procedures. To continue
the food analogy, a certain fish recipe can be prepared by either baking or broiling; the rest of the recipe is
independent. Thus, the action “desired cooking method” would be a parameter of that recipe.
Consider the function Maximum, which returns the larger of its two Integer operands:
FUNCTION Maximum (Value1, Value2: Integer) RETURN Integer IS
Result: Integer;
BEGIN
IF Value1 > Value2 THEN Result := Value1; ELSE Result := Value2; END IF;
RETURN Result;
END Maximum;
We would like to create a function that returns the larger of its two operands regardless of the types of
these operands. As in the case of Generic_Swap, we can use a generic type parameter to indicate that an
instance can be created for any type. This is not enough, however. The IF statement compares the two
input values. Suppose the type we use to instantiate does not have an obvious, predefined “greater than”
operation? Suppose the type is a user-defined record with a key field, for example? “Greater than” is not
predefined for records! We can surely write such an operation, but we need to tell the compiler to use it;
when writing a generic, we need to reassure the compiler that all the operations used in the body of the
generic will exist at instantiation time. Let us indicate in the generic specification that a comparison
function will exist.
Program 5.4 is the desired generic specification. The WITH syntax shown here takes getting used to,
but it works.
Program 5.
Specification for Generic Maximum Function
GENERIC
TYPE ValueType IS PRIVATE; WITH FUNCTION Compare(L, R : ValueType) RETURN Boolean;
FUNCTION Maximum_Generic(L, R : ValueType) RETURN ValueType;
--| Specification for generic maximum function --| Author: Michael B. Feldman, The George Washington University --| Last Modified: September 1995
The body of the generic function, presented as Program 5.5, looks similar to the one just given for
Maximum.
Program 5.
Body of Generic Maximum Function
FUNCTION Maximum_Generic(L, R : ValueType) RETURN ValueType IS
--| --| Body of generic maximum function --| --| Author: Michael B. Feldman, The George Washington University --| Last Modified: September 1995
BEGIN -- Test_Maximum_Generic
Ada.Text_IO.Put("Maximum of -3 and 7 is "); Ada.Integer_Text_IO.Put(Item => Maximum(-3, 7), Width=>1); Ada.Text_IO.New_Line; Ada.Text_IO.Put("Minimum of -3 and 7 is "); Ada.Integer_Text_IO.Put(Item => Minimum(-3, 7), Width=>1); Ada.Text_IO.New_Line(Spacing => 2);
Ada.Text_IO.Put("Maximum of -3.29 and 7.84 is "); Ada.Float_Text_IO.Put (Item => Maximum(-3.29, 7.84), Fore=>1, Aft=>2, Exp=>0); Ada.Text_IO.New_Line; Ada.Text_IO.Put("Minimum of -3.29 and 7.84 is "); Ada.Float_Text_IO.Put (Item => Minimum(-3.29, 7.84), Fore=>1, Aft=>2, Exp=>0); Ada.Text_IO.New_Line(Spacing => 2);
Ada.Text_IO.Put("Maximum of 23.65 and 37.49 is "); Currency.IO.Put (Item => Maximum(MakeCurrency(23.65), MakeCurrency(37.49))); Ada.Text_IO.New_Line; Ada.Text_IO.Put("Minimum of 23.65 and 37.49 is "); Currency.IO.Put (Item => Minimum(MakeCurrency(23.65), MakeCurrency(37.49))); Ada.Text_IO.New_Line(Spacing => 2);
END Test_Maximum_Generic;
Generic Array Parameters
An important use for generics, combined with unconstrained array types, is building very general
subprograms to deal with arrays. For a generic to be instantiated for many different array types, we need to
specify formal parameters for the index and array types.
Program 5.7 is a specification for a function Maximum_Array_Generic that returns the “largest”
of all the elements in an array, regardless of the index or element type. We place “largest” in quotation
marks because we already know that we can make it work as a minimum-finder as well.
Program 5.
Specification for Generic Array Maximum Function
GENERIC
TYPE ValueType IS PRIVATE; -- any nonlimited type TYPE IndexType IS (<>); -- any discrete type TYPE ArrayType IS ARRAY(IndexType RANGE <>) OF ValueType; WITH FUNCTION Compare(L, R : ValueType) RETURN Boolean;
FUNCTION Maximum_Array_Generic(List: ArrayType) RETURN ValueType;
--| Specification for generic version of array maximum finder --| Author: Michael B. Feldman, The George Washington University --| Last Modified: September 1995
The syntax of the specification for IndexType means “any discrete type is acceptable as an actual
parameter.” Recalling that discrete types are the integer and enumeration types and subtypes, this is exactly
what we need for the index type of the array. The specification for ArrayType looks like a type
declaration, but it is not. Rather, it is a description to the compiler of the kind of array type that is
acceptable as an actual parameter. In this case, the array type must be indexed by IndexType (or a
subtype thereof) and must have elements of type Valuetype (or a subtype thereof).
The body of Maximum_Array_Generic is shown in Program 5.8.
Program 5.
Body of Generic Array Maximum Function
FUNCTION Maximum_Array_Generic(List: ArrayType) RETURN ValueType IS
--| Body of generic array maximum finder --| Author: Michael B. Feldman, The George Washington University --| Last Modified: September 1995
Result: ValueType;
BEGIN -- Maximum_Array_Generic
FOR WhichElement IN List'Range LOOP IF Compare(List(WhichElement), Result) THEN Result := List(WhichElement); END IF; END LOOP;
RETURN Result;
END Maximum_Array_Generic;
You can write a test program for it as an exercise. As a hint, consider the following declarations:
TYPE FloatVector IS ARRAY(Integer RANGE <>) OF Float; TYPE RationalVector IS ARRAY (Positive RANGE <>) OF Rational;
and instantiate the generic as follows:
FUNCTION Maximum IS NEW Maximum_Array_Generic(ValueType=>Float, IndexType=>Integer, ArrayType=>FloatVector, Compare=>">");
FUNCTION Minimum IS NEW Maximum_Array_Generic(ValueType=>Rational, IndexType=>Positive, ArrayType=>RationalVector, Compare=>"<");
5.2 Application: A Generic Sorting Program
Let us continue our study of generics with the development of a generic sort procedure that uses much of
what we have done in this chapter. We develop a sort procedure that will work correctly for any variable of
any unconstrained array type, regardless of its bounds, index type, or element type.
In Program 4.2 we developed SelectSort, which works for any array of a particular unconstrained
array type. We need only to modify it to make it generic. We also have our procedure Swap_Generic,
which we can instantiate and use to handle exchanges.
Program 5.9 is the specification for the generic sort routine. This is similar to
Maximum_Array_Generic from Program 5.7.
Program 5.
Specification for Generic Sort Procedure
GENERIC
Using the Generic Sort to Order an Array of Records
Sort_Generic can be especially useful in sorting arrays of records. Consider the following declarations:
MaxSize : CONSTANT Positive := 250; MaxScore : CONSTANT Positive := 100;
SUBTYPE StudentName IS String(1..20); SUBTYPE ClassIndex IS Positive RANGE 1..MaxSize; SUBTYPE ClassRange IS Natural RANGE 0..MaxSize; SUBTYPE ScoreRange IS Natural RANGE 0..MaxScore;
TYPE ScoreRecord IS RECORD Name: StudentName; Score: ScoreRange; END RECORD;
TYPE ScoreArray IS ARRAY (ClassIndex RANGE <>) OF ScoreRecord;
Here is a “compare” function that tells us whether one record is “less than” another (in the sense that
one score is lower than the other):
FUNCTION ScoreLess(Score1, Score2 : ScoreRecord) RETURN Boolean IS BEGIN RETURN Score1.Score < Score2.Score; END ScoreLess;
This function compares the score fields of the two records, returning True if the first record is “less than”
the second and False otherwise. We could have named this function "<", of course, but chose not to do
so in the interest of clarity. Given Sort_Generic, it takes only a single instantiation statement to create
a sort that will order an array of score records in ascending order:
PROCEDURE SortUpScores IS NEW Sort_Generic (ElementType => ScoreRecord, IndexType => ClassIndex, ListType => ScoreArray, Compare => ScoreLess);
Given variables Scores and ClassSize as follows:
Scores: ScoreArray(ClassIndex'First..ClassIndex'Last); ClassSize: ClassRange;
we see that Scores can hold up to 250 records, and ClassSize can be used to determine the actual
number of records read from a file into the array. The array can easily be put into ascending order by score,
just by calling SortUpScores with the appropriate array slice:
SortUpScores(List => Scores(1..ClassSize));
Program 5.11 demonstrates the sort for two entirely different array types: an array of float values and an
array of phone call records like the one we used in Section 4.3.
Program 5.
Test of Generic Sort Procedure
WITH Ada.Text_IO; WITH Ada.Integer_Text_IO;
WITH Ada.Float_Text_IO;WITH Sort_Generic;
PROCEDURE Test_Sort_Generic IS
--| Demonstrates Sort_Generic using two unrelated kinds of lists; --| this is not a realistic application, but rather just shows that --| many instances of a generic can occur within one client program. --| Author: Michael B. Feldman, The George Washington University --| Last Modified: September 1995
SUBTYPE Index IS Integer RANGE 1..10; TYPE FloatVector IS ARRAY(Index RANGE <>) OF Float;
V1 : FloatVector(1..10);
SUBTYPE DayRange IS Natural RANGE 0..6; SUBTYPE Weekdays IS DayRange RANGE 0..4; SUBTYPE Weekend IS DayRange RANGE 5..6;
TYPE Days IS (Mon, Tue, Wed, Thu, Fri, Sat, Sun); TYPE CallRecord IS RECORD DayOfWeek : Days; NumberOfCalls: Natural; END RECORD;
TYPE Callers IS ARRAY(DayRange RANGE <>) of CallRecord;
PACKAGE Days_IO IS NEW Ada.Text_IO.Enumeration_IO(Enum => Days);
ThisWeek: Callers(DayRange);
-- if we are going to sort CallRecords, -- we need to know how to compare them
FUNCTION "<" (L, R: CallRecord) RETURN Boolean IS BEGIN RETURN L.NumberOfCalls < R.NumberOfCalls; END "<";
FUNCTION ">" (L, R: CallRecord) RETURN Boolean IS BEGIN RETURN L.NumberOfCalls > R.NumberOfCalls; END ">";
-- local procedures to display the contents of two kinds of lists
PROCEDURE DisplayCallers (List: Callers) IS BEGIN -- DisplayCallers FOR Count IN List'Range LOOP Days_IO.Put (Item=>List(Count).DayOfWeek, Width=>3); Ada.Integer_Text_IO.Put (Item=>List(Count).NumberOfCalls, Width=>4); Ada.Text_IO.New_Line; END LOOP; Ada.Text_IO.New_Line; END DisplayCallers;
PROCEDURE DisplayFloatVector (V: FloatVector) IS BEGIN FOR Count IN V'First..V'Last LOOP Ada.Float_Text_IO.Put(Item=>V(Count), Fore=>4, Aft=>2, Exp=>0); END LOOP;
DisplayCallers(List => ThisWeek);Ada.Text_IO.New_Line;
SortUpCallers(List => ThisWeek); Ada.Text_IO.Put(Item=> "Here is ThisWeek after upward sorting."); Ada.Text_IO.New_Line; DisplayCallers(List => ThisWeek); Ada.Text_IO.New_Line;
SortDownCallers(List => ThisWeek); Ada.Text_IO.Put(Item=> "Here is ThisWeek after downward sorting."); Ada.Text_IO.New_Line; DisplayCallers(List => ThisWeek); Ada.Text_IO.New_Line;
END Test_Sort_Generic;
5.3 Application: A Generic Binary Search Program
Sorting and searching are both very important applications in computing. Having examined a simple
generic sorting program, we will now look at a generic version of binary search, which was discussed in
Section 3.2. Recall that it is meaningful to use binary search only on a table of sorted values, and that the
“big O” of binary search is log N.
We choose to show an iterative version, rather than the recursive one given earlier, both for the sake of
variety and also because the iterative version does not require O(log N ) levels of recursion. For large N , this
would be a large number of recursive calls; it is better to avoid these, especially because the iterative
algorithm is as easy to understand as the recursive one.
Specification of the Generic Binary Search
Program 5.12 shows the generic specification for the generic binary search.
Program 5.
Specification for Generic Binary Search Procedure
GENERIC
TYPE KeyType IS PRIVATE; TYPE ElementType IS PRIVATE; TYPE IndexType IS (<>); TYPE ListType IS ARRAY (IndexType RANGE <>) OF ElementType; WITH FUNCTION "<"(Left, Right: KeyType) RETURN Boolean; WITH FUNCTION KeyOf (Element: ElementType) RETURN KeyType;
PROCEDURE Binary_Search_Generic (List : IN ListType; Target : IN KeyType; Location: OUT IndexType; Found : OUT Boolean);
--| Performs an iterative binary search of an ordered array of
--| keys with bounds List'First..List'Last.
--| Pre : Target and List are defined, and List is sorted upward --| Post: If Target is found in array List, returns True in Found --| and the location in Location; otherwise, --| returns False in Found and returns in Location --| the location in which to insert Target
--| Raises: Ada will raise Constraint_Error--| if List'Last = IndexType'Last and Target would be
--| inserted beyond List'Last. --| Author: Michael B. Feldman, The George Washington University --| Last Modified: October 1995
Let’s discuss the six generic parameters. Here, we take a slightly different approach from that of the
sort program. In general, we will be searching arrays of records, usng some field in the record as the search
key. Let’s call the key and record types KeyType and ElementType, respectively. As in the sort
program, we also specify the index and the list types. The last two parameters are given by
WITH FUNCTION "<"(Left, Right: KeyType) RETURN Boolean; WITH FUNCTION KeyOf (Element: ElementType) RETURN KeyType;
The formal comparison parameter, shown here as "<", compares keys , not elements. We therefore
provide a sixth parameter, KeyOf, so that the client can specify exactly how to find the key field of an
element.
This binary search is, in another way, more general than the simple one given in Section 3.2. That
program returns 0 if the search is unsuccessful, whereas this one returns, in the OUT parameter Location,
the location into which to insert this value. The client can then choose whether or not to actually perform
the insertion. This generality will make it possible to use binary search in the generic table handler we
present in Section 5.8.
Before we examine the body of the binary search procedure, let’s see how it might be instantiated and
used. Program 5.13 shows a test of the generic search procedure, instantiating for a record consisting of a
name and a test score.
Program 5.
A Test of Generic Binary Search
WITH Ada.Text_IO; WITH Ada.Integer_Text_IO; WITH Binary_Search_Generic; PROCEDURE Test_Binary_Search IS
--| Test of generic binary search, array of records --| Author: Michael B. Feldman, The George Washington University --| Last Modified: October 1995
Success: Boolean; WhereFound: Positive;
SUBTYPE NameType IS String(1..10); SUBTYPE ScoreType IS Natural RANGE 0..100;
TYPE ScoreRecord IS RECORD Name: NameType; Score: ScoreType := 0; END RECORD;
TYPE ScoreArray IS ARRAY(Positive RANGE <>) OF ScoreRecord;
Test1: ScoreArray(1..9);
FUNCTION NameOf (Item: ScoreRecord) RETURN NameType IS BEGIN RETURN Item.Name; END NameOf;
END IF;
BinarySearch(Test1, "Mary ", WhereFound, Success); IF Success THEN Ada.Text_IO.Put(Item => "Mary is at location "); Ada.Integer_Text_IO.Put(Item => WhereFound, Width => 1); Ada.Text_IO.New_Line; ELSE Ada.Text_IO.Put(Item => "Mary would be at location "); Ada.Integer_Text_IO.Put(Item => WhereFound, Width => 1); Ada.Text_IO.New_Line; END IF;
BinarySearch(Test1, "Zachary ", WhereFound, Success); IF Success THEN Ada.Text_IO.Put(Item => "Zachary is at location "); Ada.Integer_Text_IO.Put(Item => WhereFound, Width => 1); Ada.Text_IO.New_Line; ELSE Ada.Text_IO.Put(Item => "Zachary would be at location "); Ada.Integer_Text_IO.Put(Item => WhereFound, Width => 1); Ada.Text_IO.New_Line; END IF;
END Test_Binary_Search;
To specify how to find the key part of a score record, we write
FUNCTION NameOf (Item: ScoreRecord) RETURN NameType IS BEGIN RETURN Item.Name; END NameOf;
and use this function as an actual parameter in the instantiating statement
PROCEDURE BinarySearch IS NEW Binary_Search_Generic(ElementType => ScoreRecord, KeyType => NameType, IndexType => Positive, ListType => ScoreArray, "<" => "<", KeyOf => NameOf);
You should examine the various cases used in the test program and try to predict the response for each
case.
Now suppose the array we are searching contains simple integer elements.
TYPE IntegerArray IS ARRAY(Positive RANGE <>) OF Integer;
In this case, there is no actual key, because there is no record whose field is the key. However, the
binary search procedure requires a KeyOf function to fill that parameter. In this case, we can write a
“dummy” function that simply returns the value of its integer input.
FUNCTION Identity (Value: Integer) RETURN Integer IS BEGIN RETURN Value; END Identity;
Now we can instantiate Generic_Binary_Search, as follows:
PROCEDURE BinarySearch IS NEW Binary_Search_Generic(ElementType => Integer, KeyType => Integer, IndexType => Positive, ListType => IntegerArray, "<" => "<", KeyOf => Identity);
Modifying the test program to fit this case is left as an exercise.
Body of the Generic Binary Search
Look now at Program 5.14, the body of the search procedure.
Program 5.
Body of Generic Binary Search Procedure
PROCEDURE Binary_Search_Generic (List : IN ListType; Target : IN KeyType; Location: OUT IndexType; Found : OUT Boolean) IS
--| Body of Generic Binary Search Procedure --| Author: Michael B. Feldman, The George Washington University --| Last Modified: October 1995
Middle : IndexType; -- the subscript of the middle element Success: Boolean; Left : IndexType; Right : IndexType;
BEGIN -- Binary_Search_Generic
Left := List'First; Right := List'Last; Success:= False;
IF Target = KeyOf(List(Left)) THEN Found := True; Location := Left; ELSIF Target < KeyOf(List(Left)) THEN -- Target goes in pos. 1 Found := False; Location := Left; ELSIF Target = KeyOf(List(Right)) THEN Found := True; Location := Right; ELSIF NOT (Target < KeyOf(List(Right))) THEN -- Target beyond end Found := False; Location := IndexType'Succ(Right); ELSE WHILE (Left <= Right) AND (NOT Success) LOOP
Middle := IndexType'Val((IndexType'Pos(Left)
- IndexType'Pos(Right)) / 2);
IF Target = KeyOf(List(Middle)) THEN Success := True; ELSIF Target < KeyOf(List(Middle)) THEN Right := IndexType'Pred(Middle); -- search lower subarray ELSE
members. A set is said to be empty if it has no members. In cases where there is no ordering, it also makes
no difference if we name a member twice, so {a, b, a} = {b, a, b} = {a, b}.
Operations on Sets
What are the important operations associated with sets? Certainly inserting a member in a set and deleting a
member from a set are essential; so are testing a set to see whether a given element is a member of it and
testing a set to see whether it is empty. The last two operations are predicate or inquiry selector operations;
they return true/false values. The most important dyadic constructor operations are
- The union of two sets S and T (usually written as S ∪ T ), which returns the set containing all of S ’s
members and all of T ’s members
- The intersection of S and T ( S ∩ T ), which returns the set containing all elements which are members
of both S and T
- The difference S – T , which returns the set containing all elements which are members of S but not of
T
An often-used monadic constructor is the complement - S , which returns the set containing all elements
in the universe that are not members of S.
We will use “+” and “*” to represent the union and intersection, respectively, because the union and
intersection symbols are not part of the normal Ada character set. For example, if the universe is the letters
a..k inclusive and S = {a, d, e, g} and T = {b, c, d, e, k}, then
S + T = {a, b, c, d, e, g, k}
S * T = {d, e}
S - T = {a, g} and T - S = {b, c, k}
- S = {b, c, f, h, i, j, k}
Finally, two more inquiry operations are commonly used:
- The improper subset operation ( S C T ) which returns True if and only if all members of S are also
members of T
- The proper subset operation ( S C T ) which returns True if and only if S C T and S /= T ; that is, at
least one member of T is not a member of S.
Because the subset symbols are also missing from the Ada character set, we use < and < for improper
and proper subset, respectively. For example {b, c} < {a, b, c, d, e} and {b, c} < {a, b, c, d, e} but is not a
subset of {c, e}. Also, {a, b} < {a, b}.
Specifying the Generic Set ADT
Mathematically, sets can be infinite (all the integers, for example). In programming applications, however,
it is finite sets that are most interesting. Therefore we confine ourselves to representing finite sets—
specifically, to sets taken from finite universes of integers or enumeration values. As we shall see, it is
easy to use Ada’s generic facility to build a package providing a good but more flexible approximation to
the predefined set facility of Pascal.
A universe is either an integer subtype or an enumeration type; this means that a universe also happens
to be a valid index range for arrays. Choosing a universe, we implement a set as a one-dimensional array of
Boolean values, with index range corresponding to that universe. Given a set S represented as an array S, if
a given member of the universe is a member of S , we let the corresponding element of S be True;
otherwise we let that element be False. This representation is often called the characteristic function or bit
map of a set, and is an especially compact way to represent a large set. For example, suppose we choose the
universe a ..g. Every set over this universe is represented as a Boolean array indexed 'a'..'g'; the set S
= {a, d, e, g}, specifically, is represented as
True False^ False^ True True^ False True
a b c d e f g
Now let us devise a generic Ada package for this ADT. A framework for the generic part of the
specification is
GENERIC
TYPE Universe IS (<>);
PACKAGE Sets_Generic IS
... END Sets_Generic;
The second line specifies a generic parameter that can match any discrete type—that is, any
enumeration type or integer subtype. This is exactly what we need for our finite, discrete universes!
Program 5.15 gives the desired specification, complete with PRIVATE part defining the type Set.
Making sets a PRIVATE type allows client programs to copy sets and check them for inequality using the
predefined assignment, equality, and inequality operations, but denies clients direct access to the
implementation of sets. This leaves the package writer the flexibility to change the implementation of sets
without requiring any code changes in client programs.
Program 5.
Specification for Generic Set Package
GENERIC
TYPE Universe IS (<>); -- any integer or enumeration type
PACKAGE Sets_Generic IS
--| Specification for sets over discrete universes --| Author: Michael B. Feldman, The George Washington University --| Last Modified: October 1995
TYPE Set IS PRIVATE; Phi: CONSTANT Set; -- empty set
-- constructors
FUNCTION "+" (S: Set; E: Universe) RETURN Set; FUNCTION "-" (S: Set; E: Universe) RETURN Set; -- Pre: S and E are defined -- Post: returns S with E inserted or deleted respectively;
-- "+" has no effect if IsIn(S,E); "-" has none if NOT IsIn(S,E)
FUNCTION Singleton(E: Universe) RETURN Set; FUNCTION "+" (E1, E2: Universe) RETURN Set; -- Pre: E, E1, and E2 are defined