CS 214 Lab 10: Inheritance, Polymorphism in Ada


Begin by creating a new subdirectory named ada within your directory for this exercise. Then copy the files from the course labs/10/ada directory into your new directory. Open these files, and take a moment to study them.

A Bird Type

In the file bird_package.ads, you should see the basic skeleton for a Bird_Package:
   package Bird_Package is

   // ... declarations omitted ...

   private

   end Bird_Package;
Given our design from the introduction, we might start by creating a private Bird_Type type, with the usual partial public declaration:
   package Bird_Package is

    type Bird_Type is private;

   private

    type Bird_Type is
          record
            My_Name : String(1..6);
          end record;

   end Bird_Package;
This gives us an Ada-style ADT named Bird_Type, for which we can declare the operations required by our design:
  package Bird_Package is

    type Bird_Type is private;
 
    -- ... documentation omitted ...
    procedure Init(A_Bird : out Bird_Type; Name : in String);

    function  Name(A_Bird : in Bird_Type) return String;

    function  Call(A_Bird : in Bird_Type) return String;

    function  Type_Name(A_Bird : in Bird_Type) return String;

    procedure Put(A_Bird : in Bird_Type);

   private

     type Bird_Type is
          record
            My_Name : String(1..6);
          end record;
   end Bird_Package;
Given this much, the program in birds.adb can say

   ...
   Bird1 : Bird_Type;
   ...
and compile correctly.

Initialization. It is the responsibility of our Bird_Type to provide a means of initializing its "data member." In Ada, we can do this via an initialization procedure, which we will call Init(). As a Bird_Type operation, we define this in the body of bird_package (i.e., bird_package.adb):


   procedure Init(A_Bird : out Bird_Type; Name : in String) is
   begin
    A_Bird.My_Name := Name;
   end Init;
Add this definition in the appropriate place within bird_package.adb. Given this definition, we can 'uncomment' the first Init() in birds.adb:
   ...
   Bird1 : Bird_Type;
 begin
   Init(Bird1, "Tweety");
   ...
and the myName member of Bird1 will be initialized to Tweety.

The Accessor Function. Once we are able to initialize the myName member, it is useful if we can access its value. Back in bird_package.adb, add the following definition:


   function Name(A_Bird : in Bird_Type) return String is
   begin
    return A_Bird.My_Name;
   end Name;
Given this definition, a programmer can now write:
   Name(Bird1)
to retrieve myName from Bird1 (but don't bother doing so just yet).

The Bird Call() Function. Since every bird has a bird call, our Bird_Type ADT should provide at least a default as one of its operations. Here is a simple definition:


   function Call(A_Bird : in Bird_Type) return String is
   begin
    return "Squawwwwwwk!";
   end Call;
If each type derived from Bird_Type overloads this function with its own meaning, then a programmer can write:
   Call(someBird)
to retrieve the bird call of someBird (but don't do so yet).

The Name of the Class. While not strictly necessary, it is often useful when dealing with type hierarchies to be able to identify the particular type of which an object is a member. For example, given the following function definition:


   function Type_Name(A_Bird : in Bird_Type) return String is
   begin
    return "Bird";
   end Type_Name;
and if each type derived from Bird overrides this function with its own definition, then a programmer can write:
   Type_Name(someBird)
to determine the type of someBird.

The Output Procedure. To display the information about a Bird, we might write the following function definition:


   procedure Put(A_Bird : in Bird_Type) is
   begin
    Put( Name(A_Bird) );
    Put( ' ' );
    Put( Type_Name(A_Bird) );
    Put( " says " );
    Put( Call(A_Bird) );
   end Put; 
Why do we use our operations, rather than accessing My_Name, "Bird", and "Squawwwwwwk!" directly? Because operations can behave polymorphically, but variable and literal accesses cannot. Using our operations instead of a variable or literal is thus necessary if we want the subtypes of Bird_Type to realize the benefits of polymorphism.

At this point, you should be able to "uncomment" the first Put() statement in birds.adb, and test what you have written for syntax errors.

Note: The provided Makefile will help once you are compiling all of the packages in today's lab, but does too much here at the beginning. Instead, compile just the Bird_Package and the driver program using:

   gnatmake birds.adb
Using this, verify that what you have is correct before proceeding.

The Duck_Type Type

Thanks to the work we invested in building our Bird_Type ADT, we need only derive Duck_Type from Bird_Type to recoup our investment. Open duck_package.ads and add the declarations below, as specified by our design:

   with Bird_Package; use Bird_Package;

   package Duck_Package is

    type Duck_Type is new Bird_Type with private;


    -- ... documentation omitted ... 

    function  Call(A_Duck : in Duck_Type) return String;

    function  Type_Name(A_Duck : in Duck_Type) return String;

   private

     type Duck_Type is new Bird_Type with
          record
            null;
          end record;

   end Duck_Package;
The implementation details of Duck_Type specify that it is derived from Bird_Type, and so inherits all of its fields and operations. Note that because our design calls for no additional "data members", our Duck_Type extends Bird_Type with a record containing a null field. When a subtype actually extends its parent type, "data members" of arbitrary types can be placed here.

The Duck_Type Initializer. Our Duck_Type needs no initialization subprogram, because it has no "data members" beyond those it inherits from Bird_Type. The Init() procedure our subtype inherits from Bird_Type will thus be adequate for initialization. We can thus add the following code to birds.adb to test what we have written:

 ...
 Bird1 : Bird_Type;  
 Bird2 : Duck_Type; 

begin
 Init(Bird1, "Tweety");
 Put(Bird1); New_Line;

 Init(Bird2, "Donald");
 ...
end Birds;
Unfortunately, when we compile this code, we learn that we have a problem. Ada requires that a parent type state that subtypes may be derived from it. We thus must go back to bird_package.ads and make a few changes.

To state that subtypes may be derived from it, an Ada type must be given the tagged label. Make the following modifications to bird_package.ads:

   package Bird_Package is

    type Bird_Type is tagged private;

    -- ... declarations omitted ...

   private
     type Bird_Type is
          tagged record
            My_Name : String(1..6);
          end record;

   end Bird_Package;
This tagged designation informs that Ada compiler that other types may be derived from Bird_Type, and so that Ada compiler will add support for polymorphism to this type.

Note that if a subtype has its own "local data members", then it must provide its own initialization procedure, which should invoke that of its parent type to initialize its inherited "data members."

Rebuild birds.adb to recheck what you have written, continuing when it is correct.

The Duck_Type Bird Call. In order for a Duck_Type object to elicit the correct bird call, our Duck_Type ADT must provide its own definition of the Call() function. In duck_package.adb, we can define:


   function Call(A_Duck : in Duck_Type) return String is
   begin
    return "Quack!";
   end Call;
Even though this function's parameter type is not Bird_Type, this definition overloads our Bird_Type Call() function, because a Duck_Type is a Bird_Type.

To finish off this operation, place a declaration of this function in the appropriate place in the Duck_Package package specification.

Given this definition, a programmer can now write

   Call(someDuck)
and to elicit the bird call of the someDuck.

The Duck_Type Type Name. In order for the Type_Name() function to correctly identify the type of a Duck_Type object, we must overload it with a Duck_Type-specific definition:


   function Type_Name(A_Duck : in Duck_Type) return String is
   begin
    return "Duck";
   end Type_Name;
Place this definition in the Duck_Package package body, and the corresponding declaration in the package specification.

Once this has been done, the expression

   Type_Name(someDuck)
can be used to retrieve the string "Duck_Type".

Eliciting Polymorphism. In birds.adb, uncomment the second Put() procedure call, and test what we have written thus far. Does it do what we want?

The problem is that we have defined Put() to output a Bird_Type value, and so the compiler is binding the calls in Put(Bird_Type) to Call(Bird_Type) and Type_Name(Bird_Type), instead of Call(Duck_Type) and Type_Name(Duck_Type). Just as C++ requires us to do something special to enable polymorphism, Ada requires that a potentially polymorphic parameter be designated with the 'Class attribute. So we must modify our declaration of Put() in Bird_Package as follows:

   procedure Put(A_Bird : in Bird_Type'Class);
The definition in the package body must also be modified the same way. Make these changes, recompile, and birds.adb should behave as expected. Continue when it does.

The Goose_Type

The Goose_Type is similar to our Duck_Type. In goose_package.ads, we declare:
   with Bird_Package; use Bird_Package;

   package Goose_Package is

    type Goose_Type is new Bird_Type with private;

    -- ... documentation omitted ... 

    function  Call(A_Goose : in Goose_Type) return String;

    function  Type_Name(A_Goose : in Goose_Type) return String;

   private

     type Goose_Type is new Bird_Type with
          record
            null;
          end record;

   end Goose_Package;
The remaining functionality is inherited from class Bird. In goose_package.adb, we define these methods:
   package body Goose_Package is

    -- ... documentation omitted ... 

    function Call(A_Goose : in Goose_Type) return String is
    begin
     return "Honk!";
    end Call;

    function Type_Name(A_Goose : in Goose_Type) return String is
    begin
     return "Goose";
    end Type_Name;

   end Goose_Package;
Add this code, and then uncomment the appropriate lines in birds.adb to test what you have written, continuing when it is correct.

The Owl_Type

The Owl_Type ADT is similar to that of the Duck_Type and Goose_Type ADTs. Take a few minutes to derive it from Bird_Type, and declare and define its operations.

Testing

Test your code by uncommenting the remaining lines of birds.adb and then compiling and executing it. A Makefile has been provided to simplify the translation. What happens? Does the desired behavior occur?

Polymorphism in Ada

When the Ada compiler processes a function call like
   Put(Bird3)
the call to Put() is bound to Put(Bird_Type), because that is the only definition available. Within procedure Put(), the operations Name(), Call(), and Type_Name() are invoked. However, because the parameter of Put() is defined as
  A_Bird : in Bird_Type'Class
the particular versions of Call() and Type_Name() that are invoked depend on the type of object received via parameter A_Bird: a Duck_Type object invokes Call(Duck_Type) and Type_Name(Duck_Type); a Goose_Type object invokes Call(Goose_Type) and Type_Name(Goose_Type), and so on. Ada thus handles polymorphism somewhat different than our other languages: polymorphic behavior must be elicited within a parameterized procedure, and if the parameter for which polymorphic behavior is required is of type T, then it must be declared with the type T'Class. Ada thus triggers polymorphic behavior through modification of a parameter declaration, rather than through modification of a subprogram declaration like C++.

Turn In. When birds.adb works correctly, create a script file in which you show that it compiles correctly, run it, use cat to display the contents of each of your files.

That concludes the Ada part of this lab.


Calvin > CS > 214 > Labs > 10 > Ada


This page maintained by Joel Adams.