Tuesday, September 4, 2012

AdaTutor - Subprograms and Packages (3)

Information Hiding: Private Types

Information hiding has nothing to do with secrecy; it means containing certain programming details to a package so that the parts of the program outside the package can't depend on them.&nbp; Thus, when these details change, other parts of the program aren't affected.

Let's write the specification (only) for a graphics CRT controller package without information hiding.  Then we'll improve on it by using a private type.

The package will let us create arrays in memory representing screens.  We can create as many of these "virtual screens" as we like, and draw dots, lines, and boxes on them in memory.  We can also display any virtual screen.  Here's the package specification:

package CRT_Controller is
   type Color_Type  is (White, Black, Red, Yellow, Green, Blue);
   type Screen_Type is array(0 .. 479, 0 .. 639) of Color_Type;
   type Point is record
      Row    : Integer range 0 .. 479;
      Column : Integer range 0 .. 639;
   end record;
   procedure Display (Screen : in Screen_Type);
   procedure Clear   (Screen : in out Screen_Type;
                      To     : in Color_Type := Black);
   procedure Dot     (Screen : in out Screen_Type;
                      Place  : in Point;
                      Color  : in Color_Type);
   procedure Line    (Screen : in out Screen_Type;
                      From, To : in Point;
                      Color  : in Color_Type);
   procedure Box     (Screen : in out Screen_Type;
                      Corner1, Corner2 : in Point;
                      Color  : in Color_Type);
end CRT_Controller;

This package assumes that the resolution of the CRT is 480 vertically by 640 horizontally.  A screen is represented in memory by a two-dimensional array, each element of which contains one of six possible colors.  Procedures to draw other figures (circles, etc.) could have been added to the package.

That package specification makes the calling program very clear!  For example,

   with CRT_Controller; use CRT_Controller;
   procedure CRT_Demo is
      S1, S2 : Screen_Type;
   begin
      Clear(S1);
      Clear(S2, To => White);
      Line(S1, From => (240, 160), To => (240, 480),
           Color => Green);
      Dot(S1, Place => (240, 320), Color => Red);
      Display(S1);
      Box(S2, Corner1 => (384, 160), Corner2 => (96, 480),
          Color => Blue);
      Dot(S2, Place => (240, 320), Color => Yellow);
      Display(S2);
   end CRT_Demo;

The first executable line clears memory for screen S1 to all black, because of the default parameter in the specification of Clear.  The next line clears S2 to all white.  The next three lines draw a line and a dot on S1 and display S1.  The program then draws a box and a dot on S2 and displays S2.  Named notation could optionally have been used for the first parameter of each call, and for the objects of type Point, e.g., (Row => 240, Column => 160).

Question

At a sacrifice in clarity (not recommended!), the calling program CRT_Demo could draw a dot in the center of screen S1 without calling Dot.  Which of the following statements would accomplish the same thing as
   Dot(S1, Place => (240, 320), Color => Red);
  1. S1(240, 320) := Red;
  2. S1(Row => 240, Column => 320) := Red;
  3. Either of the above would work.

Although the calling program is very clear, there's one disadvantage that can be eliminated by using a private type: if the resolution of the screen is changed, the calling program is affected.  For example, the two calls to Dot are each intended to draw a dot in the center of the screen.  These calls will have to be changed when the resolution changes, or the dot will no longer be in the center.  Also, nothing prevents CRT_Demo from saying S1(Row => 240, Column => 320) := Red; directly, instead of calling Dot.  This is a disadvantage, because referencing the elements of the array directly makes CRT_Demo very susceptible to changes in the package.

It would be better to force CRT_Demo to call our procedure Dot, and make it impossible for CRT_Demo to use the fact the a screen is represented by a two-dimensional array.  Then the representation of a screen can change without affecting the correctness of CRT_Demo.

We'll improve our package specification and calling program to make Screen_Type a private type, so that its details can be used only inside the package.  Since the screen resolution won't be available outside the package, we'll normalize Row and Column, making them floating point numbers in the range 0.0 .. 1.0.

Here's our improved package specification:

package CRT_Controller is
   type Color_Type  is (White, Black, Red, Yellow, Green, Blue);
   type Screen_Type is private;
   type Point is record
      Row    : Float range 0.0 .. 1.0;
      Column : Float range 0.0 .. 1.0;
   end record;
   procedure Display (Screen : in Screen_Type);
   procedure Clear   (Screen : in out Screen_Type;
                      To     : in Color_Type := Black);
   procedure Dot     (Screen : in out Screen_Type;
                      Place  : in Point;
                      Color  : in Color_Type);
   procedure Line    (Screen : in out Screen_Type;
                      From, To : in Point;
                      Color  : in Color_Type);
   procedure Box     (Screen : in out Screen_Type;
                      Corner1, Corner2 : in Point;
                      Color  : in Color_Type);
private
   type Screen_Type is array(0 .. 479, 0 .. 639) of Color_Type;
end CRT_Controller;

Outside the package, there are only four things a calling program may do with objects of a private type like Screen_Type:

  1. Create them: S1, S2 : Screen_Type;
  2. Assign them: S1 := S2;
  3. Test them for equality and inequality: if S1 = S2 ... if S1 /= S2 ...
  4. Use any procedures, functions, and infix operators provided by the package: Clear(S1); Display(S2); etc.

Note that the calling program, outside the package, can no longer say S1(240, 320) := Red;, because that's not one of the four things listed above.  The calling program is forced to call Dot.  The information that Screen_Type is an array can be used by code only inside the package.  Of course, the compiler uses this information when compiling code outside or inside the package.  That's why the definition of Screen_Type must be in the package specification, not the body.  But the programmer isn't allowed to use this information outside the package.

Inside the package there are no such restrictions.  To write the bodies of the subprograms, we'll have to use the structure of Screen_Type and write statements similar to S1(240, 320) := Red;.

Here's our calling program, revised to agree with the improved package specification:

   with CRT_Controller; use CRT_Controller;
   procedure CRT_Demo is
      S1, S2 : Screen_Type;
   begin
      Clear(S1);
      Clear(S2, To => White);
      Line(S1, From => (0.5, 0.25), To => (0.5, 0.75),
           Color => Green);
      Dot(S1, Place => (0.5, 0.5), Color => Red);
      Display(S1);
      Box(S2, Corner1 => (0.8, 0.25), Corner2 => (0.2, 0.75),
          Color => Blue);
      Dot(S2, Place => (0.5, 0.5), Color => Yellow);
      Display(S2);
   end CRT_Demo;

Now, if a change is made only to the private part of the package specification, CRT_Demo won't have to be revised.  It will have to be recompiled, because the Ada compiler needs the information in the private part of the package specification.  (The linker won't link with obsolete units, but will tell us what we must recompile.)  Also, the package body may have to be rewritten.  But if CRT_Demo was correct before, it will remain correct without any revision!  The effects of the change are confined to the package.  Containing the effects of changes by means of information hiding is one of Ada's greatest features.

Question

True or False?  If the private part of a package specification is changed, the calling program must be recompiled before the package body is recompiled.

If a package exports a constant of a private type, the constant is declared in the "public" part of the package specification, but it can't be assigned a value until we get to the private part.  This is called a deferred constant.   For example,

   package CRT_Controller is
      type Color_Type  is (White, Black, Red, Yellow, Green, Blue);
      type Screen_Type is private;
      Flag_Of_Italy : constant Screen_Type;
      procedure Display (Screen : in Screen_Type);
      ...
   private
      type Screen_Type is array(0 .. 479, 0 .. 639) of Color_Type;
      Flag_Of_Italy : constant Screen_Type :=
         (others => (0 .. 213 => Green, 214 .. 426 => White,
                     427 .. 639 => Red));
   end CRT_Controller;
CRT_Demo could then say S1 := Flag_Of_Italy; or Display(Flag_Of_Italy);.

Type Text and Limited Private Types

Earlier we remarked that Ada strings are of fixed length.  Ada 95 comes with several string handling packages not found in Ada 83.  All of them are described in Annex A.4 of the Ada 95 LRM.  The most important of these are

Fixed-Length String Handling:    Ada.Strings.Fixed
Bounded-Length String Handling:    Ada.Strings.Bounded
Unbounded-Length String Handling:    Ada.Strings.Unbounded

Since Ada 83 doesn't have these packages, section 7.6 of the Ada 83 RM suggested that we create a type Text to get around the fact that Ada strings have fixed length.  Instead of declaring objects to be Strings, we'll declare them to be of type Text, which will simulate variable-length strings.  Then we'll see how this creates a need for limited private types.

The package specification in section 7.6 of the Ada 83 RM makes use of discriminated records.  Since we haven't yet covered that topic, we'll give a simplified presentation here.  Our type Text corresponds closely to the type Bounded_String in the Ada 95 package Ada.Strings.Bounded.

Let's declare

      type Text is record
         Len : Integer range 0 .. 80 := 0;
         Val : String(1 .. 80);
      end record;
Any appropriate maximum length may be used in place of 80.  It isn't necessary to initialize the Val field to all blanks, because the Len field keeps track of how much of the Val field is significant.  The Len field is given a default value of 0, so that objects of type Text will be initialized to zero length.

For example, we could declare T1 : Text;.  We could then set T1.Len := 5; and T1.Val(1 .. 5) := "Hello";.  The fact that the last 75 characters of T1.Val might contain garbage is of no consequence, because T1.Len tells our program to consider only the first 5 characters of T1.Val.  For example, if our program withs and uses Ada.Text_IO, it might say Put_Line(T1.Val(1 .. T1.Len));.  Since T1.Len is a variable, we've simulated variable-length strings.

A minor disadvantage is that, for each object of type Text, we reserve enough memory for the longest possible length (80 in this example).  Discriminated records, to be covered in the section on More Records and Types, can overcome this disadvantage.

Type Text will be much easier to use if we write some subprograms.  First, we need to convert between types Text and String.  Conversion in both directions is very simple:

      function Str(T : in Text) return String is
      begin
         return T.Val(1 .. T.Len);
      end Str;

      function Txt(S : in String) return Text is
         Answer : Text;
      begin
         Answer.Len := S'Length;
         Answer.Val(1 .. Answer.Len) := S;
         return Answer;
      end Txt;

Now we can write, for example, T1 : Text := Txt("Hello"); and we don't even have to count the characters of "Hello".  Later, the program might execute T1 := Txt("Type Text is very convenient."); showing that T1 simulates a variable-length string.  If we with and use Ada.Text_IO in our program, we can write Put_Line(Str(T1));.

It would be convenient to overload the & operator to concatenate two Texts, or a Text with a String.  We can also overload four of the relational operators:

function "&"  (Left, Right : in Text)               return Text;
function "&"  (Left : in Text;   Right : in String) return Text;
function "&"  (Left : in String; Right : in Text)   return Text;
function "<"  (Left, Right : in Text)            return Boolean;
function ">"  (Left, Right : in Text)            return Boolean;
function "<=" (Left, Right : in Text)            return Boolean;
function ">=" (Left, Right : in Text)            return Boolean;

The bodies of these subprograms are very simple! For example:

   function "&"(Left, Right : in Text) return Text is
   begin
      return Txt(Str(Left) & Str(Right));
   end "&";

   function "<"(Left, Right : in Text) return Boolean is
   begin
      return Str(Left) < Str(Right);
   end "<";

Question

   function Txt(S : in String) return Text is
      Answer : Text;
   begin
      Answer.Len := S'Length;
      Answer.Val(1 .. Answer.Len) := S;
      return Answer;
   end Txt;
True or False?  A call to Txt with the null string, Txt(""), will raise a Constraint_Error.

   type Text is record
      Len : Integer range 0 .. 80 := 0;
      Val : String(1 .. 80);
   end record;

There are two problems with type Text.  The way Ada assigns arrays is less than ideal when assigning objects of type Text, and the way Ada tests for equality is totally unacceptable.  Suppose we have T1, T2 : Text; and then we execute T1 := Txt("Hello"); and then T2 := T1;.  In doing the assignment, Ada will copy all 80 characters of T1.Val, even though the last 75 characters contain garbage and only the first 5 characters (and the Len field) need be copied.  Perhaps we could live with this inefficiency, but Ada's test for equality of arrays creates a more serious problem.

If we write T1 := Txt("aaaaaaaaaa"); and T2 := Txt("bbbbbbbbbb"); and then T1 := Txt("Hello"); and T2 := Txt("Hello");, Ada will say that T1 /= T2, because it compares the entire Val fields: T1.Val(6 .. 10) is "aaaaa", but T2.Val(6 . .10) is "bbbbb".  We want Ada to compare only the first 5 characters of T1 and T2, as both Len fields are 5. We could modify function Txt (that converts from String to Text) to pad the rest of the Val field with blanks, so that the test for equality would be correct, but that would reduce efficiency.

We could also try to get around this problem by writing our own function Equal:

   function Equal(T1, T2 : in Text) return Boolean is
   begin
      return Str(T1) = Str(T2);
   end Equal;

This function would work, but we might forget to write if Equal(T1, T2) and write if T1 = T2 instead.  Similarly, we could write a procedure to assign Texts efficiently, and forget to use it and write T2 := T1;.

Ada 95 lets us write a function "=" similar to the function above:

   function "="(T1, T2 : in Text) return Boolean is
   begin
      return Str(T1) = Str(T2);
   end "=";

This gets around the problem that we might forget to write if Equal(T1, T2) and write "if T1 = T2" instead.  However, it doesn't solve the problem of assigning Texts efficiently.  For this we need a limited private type.

Ada will prevent us from assigning Texts and testing them for equality if we create a package and make type Text limited private.  Outside the package there are only two things we may do with objects of a limited private type:

  1. Create them: T1, T2 : Text;
  2. Use any procedures, functions, and infix operators provided by the package: T1 & T2 etc.

We can't test for equality and inequality unless the package includes a function "=".  Also, Ada won't let us write T2 := T1;, but the package could provide a procedure to assign Texts.  Here's our package specification:

   package Text_Handler is
      type Text is limited private;
      function Str(T : in Text) return String;
      function Txt(S : in String) return Text;
      function "&"(Left, Right : in Text) return Text;
      ...
      function "="(Left, Right : in Text) return Boolean;
      function "<"(Left, Right : in Text) return Boolean;
      ...
      procedure Set(Target : in out Text; To : in Text);
   private
      type Text is record
         Len : Integer range 0 .. 80 := 0;
         Val : String(1 .. 80);
      end record;
   end Text_Handler;

Note that we write type Text is limited private, but we still introduce the private part of the package simply with the word private.

The two new subprograms are as easy to write as the others:

   function "="(Left, Right : in Text) return Boolean is
   begin
      return Str(Left) = Str(Right);
   end "=";

   procedure Set(Target : in out Text; To : in Text) is
   begin
      Target.Len := To.Len;
      Target.Val(1 .. Target.Len) := To.Val(1 .. To.Len);
   end Set;

In summary, we used limited private for type Text because Ada's definitions of equality and assignment weren't appropriate for that type, and we wanted to provide our own definitions.  However, Ada's definitions were appropriate for Screen_Type, so we made that a private type to contain the effects of changes.

In Ada 95, the choice between private and limited private should depend on whether we need to redefine assignment, not equality, because Ada 95 lets us redefine "=" even if the type isn't limited private.

Question

   package Stacks is
      type Stack is ?
      procedure Push (S : in out Stack; Item : in  Integer);
      procedure Pop  (S : in out Stack; Item : out Integer);
   private
      type Ivector is array(Integer range <>) of Integer;
      type Stack is record
         Sp : Integer range 1 .. 11 := 1;
         St : Ivector(1 .. 10);
      end record;
   end Stacks;
Suppose we want to write a package that lets us create objects of type Stack, and Push and Pop integers on them.  The stacks will be last in, first out.  (For now, ignore the possibilities of stack underflow and overflow.)  Should type Stack be private or limited private?
  1. Type Stack should be private.
  2. Type Stack should be limited private.

Hierarchical Libraries

Earlier we considered a package CRT_Controller that had procedures to draw dots, lines, and boxes.  Suppose that we have compiled that package and several calling programs, and then we wish to add a procedure to draw a circle.  If we add the procedure directly to CRT_Controller, we'll have to recompile CRT_Controller, and, as a result, all of the previously written calling programs, because these with the package.  This is true even though the previously written calling programs don't call the new procedure to draw a circle.

In Ada 95, we can write a child package to get around this problem.  Its name is CRT_Controller followed by a dot and another name, for example, CRT_Controller.More.  (The pre-supplied package Ada.Text_IO is a child of the package Ada; that's why its name contains a dot.)  A child package must be compiled after the specification of the parent, such as CRT_Controller.  We do not change or recompile CRT_Controller.  Instead, we put our new routines, such as Circle, in CRT_Controller.More and compile it.

Now the previously written calling programs that with CRT_Controller don't have to be recompiled.  The new programs, that can call Circle (as well as Dot, Line, Box, etc.) should say with CRT_Controller.More;.  This automatically does with CRT_Controller; as well, so we don't need to mention CRT_Controller in a with statement.  However, if we want to use both the parent package and the child package, we must mention both in a use statement.

Our new Ada 95 child package specification will look like this:

   package CRT_Controller.More is
      ...
      procedure Circle ... ;
      ...
   end CRT_Controller.More;

A parent package can have several children, and the children can have children, to any depth.  While only packages can have children, the children themselves can be subprograms (procedures or functions) as well as packages.

Although subprograms can't have "children," they can, of course locally declare other subprograms, and these declarations may say is separate, even in Ada 83.

A child package can be made private to its parent.  Let's suppose that, for some reason, we wanted the resources of CRT_Controller.More (such as Circle) to be available only within the CTR_Controller family of packages and subprograms.  We can write

   private package CRT_Controller.More is
      ...
      procedure Circle ... ;
      ...
   end CRT_Controller.More;

Now units outside this family can't with CRT_Controller.More, because this package is a private child.  Also, the specifications of other children of CRT_Controller can't with CRT_Controller.More either, because if they could, they could export its resources outside the family.  However, the bodies of the other children of CRT_Controller can with CRT_Controller.More, because resources in the bodies aren't available outside the family.

Ada 95 hierarchical libraries and Ada 95 tagged types (discussed later) both help support Object Oriented Design (OOD).

< prev   next >

Post a Comment