Ada for Pascal Programmers

Getting acquainted with Ada needn't be a stressful experience especially if you're already comfortable with Pascal.


September 01, 1988
URL:http://www.drdobbs.com/tools/ada-for-pascal-programmers/184407995

SEP88: ADA FOR PASCAL PROGRAMMERS

K.N. King is an associate professor of mathematics and computer science at Georgia State University, Atlanta, GA 30303. He is the author of Modula-2: A Complete Guide.


Pascal was the starting point for the design of Ada, and Ada programs--at least superficially--resemble Pascal programs. Ada is not a superset of Pascal, however; as we'll see later, Ada is missing at least two of Pascal's features. (Incidentally, when I refer to Pascal, I mean ISO Standard Pascal. Some Pascal compilers incorporate a few Ada-like extensions.)

Although Ada is based on Pascal, Ada is a much larger language. Not only does Ada have many features but it also allows these features to be combined in a virtually unlimited number of ways. Getting started in Ada is relatively easy because of its surface resemblance to Pascal. Mastering Ada is much harder, however, because of its size and complexity.

Like Pascal, Ada provides a rich collection of data types, including array, record, and pointer types (pointer types are called access types in Ada), although Ada's array and record types have additional options not available in Pascal. Ada's control structures (the if case, while, and for statements) are similar to Pascal's. Instead of Pascal's repeat statement, however, Ada has the more general loop statement. Like Pascal, Ada provides both procedures and functions, known collectively as subprograms.

Ada, like Pascal, is a strongly typed language. Every object must have a fixed, declared type, and--in general--types must match when objects are combined in expressions. Pascal has a handful of exceptions to the rule of strong typing. In particular, Pascal allows the mixing of integer and real values in expressions; an integer value is automatically converted to type real before being combined with a real value. Ada is stricter than Pascal; values of types Integer and (Float corresponds to Pascal's real type) may not be combined in an expression without explicit type conversion.

Ada combines the security of Pascal with the power of C. Ada compilers are required to perform a great deal of type checking in an effort to catch potential errors at compile time. Yet, programmers can override type checking when it hinders access to the underlying machine.

In this article, I'll discuss several of Ada's more significant features, including overloading, packages, separate compilation, private types, exceptions, and generics. Because space limitations preclude detailed discussion of some important features--including tasking and representation specifications--I'll mention them briefly at this point.

Unlike Pascal and C (and most other common languages, for that matter), Ada provides built-in support for tasking. An Ada program may consist of many tasks, each executing--at least from the programmer's point of view--asynchronously and in parallel. Ada provides a novel mechanism (the rendezvous) for tasks to synchronize with each other and exchange information; tasks can also communicate through shared variables. Ada's tasking features include support for real-time applications.

Like Pascal, Ada normally hides machine-level details from the programmer. Unlike Pascal, however, Ada allows access to these details when absolutely necessary. In particular, Ada programs may locate a variable at a particular address or specily its size, perform unchecked type conversion, and do a host of other things that depend on knowledge of the target machine (the machine on which the program will run) and/or the particular Ada system being used.

Surprisingly for such a large language, Ada lacks two features of Pascal: support for sets and the ability to pass a subprogram as a parameter to another su'bprogram. Built-in support for sets is not as important in Ada as in Pascal because Ada programmers can always create a package that provides a set type and a collection of set operations. The advantage of Ada's do-it-yourself approach is that you can tailor the set type to your needs--in particular, you can choose the maximum size of the set and the method in which it is implemented.

Pascal vs. Ada: An Example

When used to write simple programs, Ada is not terribly different from Pascal. There are, however, a few Ada characteristics that you should keep in mind, especially if you are an experienced Pascal programmer. For one thing, input/output procedures are not built into Ada as they are in Pascal. To perform I/O, an Ada program can use one of the standard I/O packages that is provided with every Ada system.

Of Ada's I/O packages, the most often used is Text__IO, which supplies subprograms for reading and writing textual information. Text__IO subprograms include Get, Put, Skip__Line, and New__Line. Get reads one item of data, while Put writes one item. (As we'll see later, Get and Put are "overloaded"; Text__IO actually contains a number of subprograms with these names.) Skip__Line skips any characters that remain on the current input line. New__Line ends the current line of output and advances to the next line.

For a program to gain access to the subprograms in Text__IO, it must name the package in a with clause. A with clause by itself allows access to the subprograms in Text__IO, but each call of a subprogram must include the package name. For example, a call of Put would look like this:

Text__IO.Put(Ch);

Following a with clause by a use clause provides direct access to the names in Text__IO. If a use clause is present, calls of Put don't have to mention Text__IO:

Put(Ch);

An Ada main program can be either a procedure or a function (usually the former). Procedure names, like all other identifiers in Ada, may contain underscores. Like Pascal, Ada is not sensitive to the case of letters in an identifier.

In Ada, the semicolon is a statement terminator; it must appear at the end of every statement. In Pascal, the semicolon is a statement separator; it separates statements rather than terminating them. Ada strings are enclosed in double quotes; Pascal requires single quotes. Ada comments begin with the - - symbol and continue to the end of the line.

Example 1, this page, shows a Pascal program that counts the number of times each letter (either upper or lowercase) occurs in the input stream. Example 2, this page, is an equivalent Ada program.

Example 1: A Pascal program that counts occurences of letters.

    program CountLetters(input, output);
      { counts ocurrences of letters in the input stream}
      var Counts: array ['a'. .'z'] of integer;
            Ch: char;

   begin
      for Ch := 'a' tp 'z' do
          Counts (Ch) := 0;
      while not eof do
          begin
               read (Ch);
               if ('a' <= Dh) and (Ch <= 'Z') then
         Counts [Ch] := Counts[Ch] + 1
               else if ('A' <= Ch) and (Ch <= 'Z') then
         begin
               Ch := chr(ord(Ch) - ord('A') + ord ('a'));
               Counts[Ch] := Counts[Ch] +1
         end
          end;
      for Ch := 'a' tp 'z' do
          writeln(Ch, Counts[Ch]:6)
   end.

Example 2 reveals one of Ada's quirks. Subprograms for reading and writing integers are located not in Text__IO itself but in the generic package Integer__IO which is nested inside Text__IO. Lines 4 and 5 instantiate this package and provide direct access to its subprograms. I'll discuss generic packages and instantiation in more detail later. Ada arrays are similar to Pascal arrays. Notice in line 6, however, that Ada requires parentheses instead of square brackets to enclose array bounds. Also, Ada allows the use of aggregates to initialize arrays. The aggregate (others => 0) indicates that all components of the Counts array should be assigned the value 0. In line 7, notice that Ada's character type is named Character instead of char, and in line 9, that Ada's while statement has the form while ... loop ... end loop. Any number of statements may appear between the words loop and end loop. The function End__Of__File is from the Text__IO package.

Example 2: An Ada program that counts occurances of letters in the input stream

   1.   with Text_IO; use Text_IO;
   2.   procedure Count_Letters is
   3.        -- counts occurences of letters in the input stream
   4.        package Int_IO is a new Integer_IO(Integer);
   5.        use Int_IO;
   6.        Counts: array ('a'. .'z') of Integer := (others =>0);
   7.        Ch: Character;
   8.   begin
   9.        while not End_Of_file loop
   10.            Get(Ch);
   11.            if 'a' <= Ch and Ch <= 'z' then
   12.                 Counts(Ch) := Counts(Ch) + 1;
   13.            elsif 'A' <= Ch and Ch <= 'Z' then
   14.                 Ch := Character'Val (Character'Pos(Ch) -
   15.                                     Character'Pos('A') +
   16.                                     Character'Pos('a'));
   17.                 Counts(Ch) := Counts(Ch) + 1;
   18.            end if;
   19.       end loop;
   20.       for Ch in 'a'. .'z' loop
   21.            Put (Ch);
   22.            Put (Counts(Ch), 6);
   23.            New_Line;
   24.       end loop;
   25.  end Count_Letters;

We can omit the parentheses around 'a' <= Ch and Ch <= `z' in line 11 because the and operator has lower precedence than the <= operator. The parentheses are mandatory in Pascal, which has only four levels of operator precedence. Ada has six levels of precedence, with the logical operators and, or, and xor at a lower level than the relational operators <, <=, >, >=, =, and /= (not equal). In lines 11-18, notice that Ada's if statement has the form if... then ... {elsif... then ... } [else ...] end if where the braces mean that there may be any number of elsif clauses (including none) and the square brackets mean that the else clause may be omitted.

In Ada, entities (including objects and types) may have attributes. One of the attributes of the Character type is the function Character'Pos, used on lines 14-16, which is similar to Pascal's ord function. Another attribute of Character is Character'Val, which is equivalent to Pascal's chr function. The for statement in line 20, like the while statement, is a special case of the loop statement. The for statement specifies a loop parameter (Ch, in this case) and a range of values for the loop parameterto assume ('a'. .'z'). A loop parameter is not a normal variable (although it may have the same name as an ordinary variable): It need not be declared, and it is not visible outside the loop. The second parameter to the call of Put (line 22) indicates that Counts(Ch) should be written using at least six characters; if Counts(Ch) does not require six characters, extra space will precede it. This parameter is a default parameter; it may be omitted, in which case it assumes a default value. Ada subprograms may have any number of default parameters.

Overloading

One of Ada's more unusual features is its tolerance for overloading (assigning different meanings to the same symbol). Pascal has a small amount of built-in overloading (for example, the symbol + represents integer addition, real addition, and set union) but provides no support for programmer-defined overloading. Ada allows overloading of several kinds of entities, including subprogram names. When it encounters a call of an overloaded subprogram, an Ada compiler must decide which subprogram is actually being called. It makes this decision by examining the number of actual parameters and their types.

Text__IO's Get and Put procedures are heavily overloaded. For example, consider the Put procedures shown in Example 3, below. If a call of Put has two parameters, the first of type File__Type and second of type Character, then the compiler deduces that this is a call of the first version of Put. If there is only one parameter and its type is Character, then this is a call of the second version of Put.

Example 3: Overloading the Put procedure

procedure Put(File: File_Type; Item: Character);
procedure Put(Item: Character);
procedure Put(File: File_Type; Item: String);
procedure Put(Item: String);

One of the advantages of overloading is that similar subprograms can be given the same name so that programmers have fewer names to remember. If abused, however, overloading can make programs hard to read.

Packages

An Ada program consists of one or more compilation units. One of these must be a subprogram that serves as the main program; the other units may be subprograms or packages. A package is a collection of other Ada entities, including constants, types, variables, and subprograms. Each package has two parts: a specification and a body. The specification lists information to be made available to the rest of the program, and the body contains information to be hidden from the rest of the program.

A package may be nothing more than a collection of object and/or type declarations. Example 4, on page 37, shows a package called Length Conversions that contains declarations of four useful constants. This package needs no body; there is no information to be hidden.

Example 4: The specification of the Length__Conversions package

package Length_Conversions is
   Feet_To_Meters: constant := 0.3048;
   Inches_To_Centimeters: constant := 2.54;
   Miles_To_Kilometers: constant :=m 1,6093;
   Yards_To_Meters: constant := 0.9144;
end Length_conversions;

The entities declared in a package specification are available for use by other compilation units (its clients). First, however, each client must mention the package in a with clause. Example 5, page 38, shows a program named Convert__To__Meters that uses some of the constants in Length__Conversions. Notice that Convert__To__Meters gains access to the constants in Length Conversions in the same way as it gains access to the subprograms in Text__IO: through with and use clauses.

Example 5: A program that uses the Length_Conversions package

   with Text_IO, Length_Conversions;
   use Text_IO, Length_Conversions;
   procedure Convert_To_Meters is
      package Int_IO is new Integer_IO(Integer);
      use Int_IO;
      Feet: Integer;
begin
   Put ("Enter a measurement in feet: ");
   Get(Feet)
   Skip_line;
   Put("The equivalent in meters is: ");
   Put(Integer (Float (Feet) * Feet_To_Meters), 1);
   New_Line;
end Convert_To_meters;

A collection of related subprograms also makes a natural package. Example 6, page 38, shows the specification of a package (Angle Conversions) that provides functions for converting from degrees to radians and vice versa. When a package supplies subprograms, only the headings of the subprograms appear in the specification of the package. Clients of the package need to know how to call the subprograms but don't need to know how the subprograms work.

Example 6: The specification of the Angle__Conversions package

     package Angle_Conversions is
          function Degrees_To_Radians(Degrees: Float) return Float;
          function Radians_To_Degrees(Radians: Float) return Float;
     end Angle_Conversions;

The Angle__Conversions package, unlike Length__Conversions, must have a body. The body contains full declarations of the subprograms in the package. Example 7, page 39, shows the body of Angle__Conversions. Notice that an Ada function terminates by executing a return statement; the expression that follows the word return is the value that the function returns.

Example 7: The body of the Angle__Conversions package

     package body Angle_Conversions is

          Two_Pi: constant := 2.0 * 3.14159;

          function Degrees_To_radians(Degrees: Float) return Float is
          begin
               return Two_Pi * Degrees / 360.0;
          end Degrees_To_Radians;

          function Radians_To_Degrees(Radians: Float) return Float is
          begin
               return 360.0 * Radians / Two_Pi;
          end Radians_To_Degrees;

     end Angle_Cnversions;

A package may contain hidden data structures. To show how such a data structure might arise, we'll write a small program that reverses a string entered by the user. Reversing a string is easy to do using a stack (a last-in, first-out data structure): As the program reads the characters, it pushes them onto the stack; when all characters have been read, the program pops characters from the stack, writing each character as it is popped. Let's write a package that provides subprograms (representing operations on the stack) while hiding the stack itself inside the body of the package.

We will need three stack operations: Push (push a character onto the stack(, Pop (retrieve the top stack element, then pop the stack), and IsEmpty (determine whether the stack is empty). Example 8, page 39, shows a package named Char__Stack that provides these operations. A more useful stack package might include other operations (finding the top stack element without popping, testing whether the stack is full, and so forth); for simplicity, we'll limit ourselves to three operations.

Example 8 illustrates an aspect of Ada that you have not seen in previous examples. Instead of Pascal's value and variable (var) parameters, Ada provides three parameter modes: in, out, and in out. A formal parameter is declared to have mode in if it represents a value to be supplied to the subprogram; an in parameter cannot be changed by the subprogram. An out parameter represents a variable that will be assigned a value by the subprogram. An in out parameter represents a variable that can be both read and modified by the subprogram. The default mode is in. The parameter X to Push has mode in because Push needs only to read X, not modify it. The parameter X in Pop has mode out because Pop will store into X the value popped from the stack.

Example 8: The specification of the Char__Stack package

     package Char_Stack is

          procedure Push(X: Character);
               -- pushes X onto the stack

          procedure Pop(X: out Character);
               -- stores the top stack element into X, then pops the stack

          function Is_Empty return Boolean;
               -- returns True if the stack is empty, False otherwise

     end Char_Stack;

The body of Char__Stack (see Example 9, page 40) contains declarations of the objects that make up the hidden stack as well as declarations of the subprograms that operate on the stack. Notice that the value of Top__Of__Stack is constrained to lie between 0 and Stack__Size; this is similar to a Pascal subrange. For now, ignore the possibility that Top__Of__Stack might exceed Stack__Size or fall below 0; I'll address the issue of error handling later. Example 10, page 40, shows a main program that uses the Char__Stack package to reverse a string.

Example 9: The body of the Char__Stack package

     package body Char_Stack is

          Stack_Size: constant := 100; --maximum size of stack
          Stack_Array: array (1. .Stack_Size) of Character;
          Top_Of_Stack: Integer reand 0. .Stack_Size := 0;

          procedure Push(X: Character) is
          begin
               Top_Of_Stack := Top_Of_Stack + 1
               Stack_Array(Top_Of_Stack) := x;
          end Push;

          procedure Pop(X: out Character) is
          begin
               X := Stack_array(Top_OF_Stack);
               Top_Of_Stack := Top_Of_Stack - 1
          end Pop;

          function Is_Empty return Boolean is
          begin
               return Top_Of_Stack = 0;
          end Is_Empty;

     end Char_Stack;

Example 10: A program that uses Char_Stack package to reverse a string

     with Test_IO, Char_Stack;
     use Text_IO, Char_Stack;
     procedure Reverse_String is
          Ch: Character;
     begin
          Put ("Enter string to be reversed: ");
          while not End_Of_line loop
               Get(Ch);
               Push(Ch);
          end loop;
          Skip_Line;

          Put("The reversal is: ");
          while not Is_Empty loop
               Pop(Ch);
               Put(Ch);
          end loop;
          New_Line;
     end Reverse_String;

Separate Compilation

The various subprograms and packages that make up a program may be kept in separate files and compiled separately, and the specification of a package may be compiled separately from the body of the package. The order of compilation is important, however. The specification of a package must be compiled before the body of the package and before all clients of the package. (For example, we must compile the specification of Char__Stack before we compile the body of Char__Stack and before we compile Reverse__String.) This rule ensures that the compiler will have access to the information in the package specification when the body (and the clients) are compiled. Restricting the order of compilation allows an Ada compiler to perform full type checking across the boundaries of compilation units.

When the body of a package is changed and recompiled, the rest of the prograrn need not be recompiled, provided that the specification of the package did not change. Turbo Pascal 4.0 provides a package like feature known as a unit, but units lack some of the benefits of Ada packages. A Turbo Pascal unit has an "interface" section and an "implementation" section, but both sections must be kept in the same file. As a result, when the implementation of a unit changes, all clients of the unit must be recompiled, even if the interface has not changed.

Private Types

The Char__Stack package is an example of a reusable software component. Although it was written for use in the Reverse__String program, Char__Stack is general enough to be used in other programs as well. The usefulness of Char__Stack is limited, however, because it hides a single stack of characters. If we're writing a program that uses two or more stacks, we want a stack type that we can use to declare as many stack variables as needed.

We can easily modify the package to provide a Char__Stack type along with the Push, Pop, and Is__Empty subprograms. Example 11, page 43, shows the specification of the new package, which we'll call Char__Stacks. A client of Char__Stacks can declare one or more variables of type Char Stack and use the Push, Pop, and Is__Empty subprograms to operate on these variables:

S: Char__Stack;
...
Push(S, Ch);

There's a problem with this definition of the Char__Stack type, however: a Char__Stack variable is actually a record, and the client has full access to the components of this record. There is nothing to prevent the client from accessing--or even modifying--the components of a Char__Stack variable. For example, the client might bypass the Push procedure as follows:

S: Char__Stack;
...
S.Top__Of__Stack :=
          S.Top__Of__Stack + 1;
S.Stack__Array(S.Top__Of__Stack) : =
                   Ch;

This practice is dangerous because there is no guarantee that the stack will remain consistent. For example, the client might increment Top__Of__Stack without storing into Stack__Array or modify a component of Stack__Array that is not at the top of the stack.

Example 11: The specification of the Char__Stacks package; Char__Stack is an ordinary type

     package Char_Stacks is

          Stack_Size: constant := 100;
          type Array_Type is array (1. .Stack_Size) of Character;
          type Char_Stack is
               record
                    Stack_Array: Array_Type;
                    Top_Of_Stack: Integer range 0. .Stack_Size := 0;
               end record;

          procedure Push(S: in out Char_Stack ; X: Character);
               -- pushes X onto stackS

          procedure Pop(S: in out Char_Stack; X: out Character);
               -- stores the top element of S into X, then pops S

          function Is_Empty(S: Char_Stack) return Boolean;
               -- returns True if S is empty, False otherwise

     end Char_Stacks;

Another problem caused by direct manipulation of the stack is that the client becomes dependent on a single representation of the stack. We would like to retain the option of representing the stack in some other form. For example, if stack overflow is a problem, we might rewrite the Char__Stacks package so that a Char__Stack variable is a pointer to a linked list of records. If clients have used only the Push, Pop, and IsEmpty subprograms to access Char__Stack variables, then the clients will not need to be changed.

The designers of Ada recognized the problems that can occur when clients have access to the representation of a type such as Char__Stack. To prevent such access, we can declare Char__Stack to be a private type. If Char__Stack is a private type, clients cannot take advantage of the fact that a Char__Stack variable is really a record; clients can supply a Char__Stack variable to a call of Push, Pop, or Is__Empty but may not directly examine or change its components.

Example 12, page 43, shows the appearance of Char__Stacks when we make Char__Stack a private type. Notice that the full declaration of Char__Stack still appears in the package specification but is "hidden" at the end in a special section known as the private part. Information that appears in the private part of a package specification is visible to the compiler but not to clients of the package. Clients know only that Char__Stack is a type, not that it is a record type.

Example 12: The specification of the Char__Stacks package; Char__Stack is a private type

     package Char_Stacks is

          type Char_Stack is private;

          procedure Push (S: in out Char_Stack; X: Character);
               -- pushes X onto stack S

          procedure Pop(S: in out Char_Stack; X: out_Cahrater);
               -- stores the top element of S into X, then pops S

          function Is_Empty(S: Char_Stack) return Boolean;
               -- returns True if S is empty, False otherwise

     private
          Stack_Size: constant := 100;
          type Array_Type is array (1. .Stack_Ssize) of Character;
          type Char_Stack is
               record
                    Stack_Array; Array_Type;
                    Top_Of_Stack: Integer range 0. .Stack_Size := 0;
               end record;
          end Char_Stacks;

Example 13, page 44, shows the body of the Char__Stacks package. The body does not depend on whether or not Char__Stack is a private type. Example 14, page 44, shows the Reverse__String program after it has been modified to use the Char__Stacks package.

Example 13: The body of the Char_Stacks package

     package body Char_Stacks is

          procedure Push(S: in out Char_Stack; X: Character) is
          begin
               S.Top_Of_Stack := S.Top_Of_Stack + 1;
               S.Stack_Array(S.Top_Of_Stack) := X;
          end Push;

          procedure Pop(S: in out Char_Stack; X: out Character) is
          begin
               X := S.Stack_Array (S.Top_Of_Stack);
               S.Top_Of_Stack := S.Top_Of_Stack - 1;
          end Pop;

          function Is_Empty(S: Char_Stack) return Boolean is
          begin
               return S.Top_Of_Stack = 0;
          end Is_Empty;

     end Char_Stacks;

Example 14: A program that uses the Char__Stacks package to reverse a string

     with Text_IO, Char_Stacks;
     use Text_IO, Char_Stacks;
     procedure Reverse_String is
          S: Char_Stack;
          Ch: Character;
     begin
          Put ("Enter string to be reversed: ");
          while not End_Of_Line loop
               Get(Ch);
               Push(S,CH0:
          end loop;
          Skip_Line;

          Put ("The reversal is: " );
          while not Is_Empty (S) loop
               Pop(S, Ch);
               Put(Ch);
          end loop;
          New_Line;
     end Reverse_String;

Exceptions

An exception is an error (division by 0, for example) or other unusual condition that occurs during program execution. Ada provides support for naming an error condition (declaring an exception), for responding to an exception (handling the exception), and for causing an exception to occur (raising the exception).

The following exceptions are predefined in Ada: Constraint__Error, Numeric__Error, Program__Error, Storage__Error, and Tasking__Error. Of these, the most common are Constrait__Error, which is raised when any kind of constraint is violated (often a subscript out of range) and Numeric__Error, which is raised by conditions such as overflow and division by 0. Although there are only five built-in exceptions, programmers may declare others as needed.

What happens when an exception is raised during the execution of a program? If it fails to handle the exception, the program terminates. (When an exception is not handled, an Ada system normally displays the name of the exception and line number on which it was raised.) To be able to handle an exception, we must include extra code indicating what to do if a certain exception is raised at a particular point in the program. Here's one way to define an exception handler:

begin
statements in which exception might
be raised
exception
when
exception__name => code for exception handler
end;

If the named exception is raised in one of the statements between begin and exception, control is transferred to the handler; after it has executed, control is transferred to the statement just after the word end. If no exception is raised during execution of the statements between begin and exception, then everything between exception and end is skipped.

Consider the Push procedure in the Char__Stacks package. When S.Top__Of__Stack is equal to Stack__Size, incrementing S.Top__Of__Stack will raise the Constraint__Error exception, because S.Top__Of__Stack is constrained to be in the range 0. .Stack__Size. When an exception is raised and the current subprogram doesn't handle it, the subprogram terminates and the exception is raised in the calling subprogram. Since Push doesn't have a handler for the Constraint__Error exception, the caller of Push will get an opportunity to handle the exception. Unfortunately, the client must have some information about how the stack is implemented in order to know what exception will be raised. If the stack were implemented as a linked list, stack overflow might be indicated by the Storage__Error exception instead of Constraint__Error.

Because of problems such as this, Ada allows the declaration of new exceptions. If an exception is declared in the specification of a package, it becomes available to clients of the package. In the case of the Char__Stacks package, there are two errors that could occur during the use of the package: overflow (attempting to push when the stack is full) and underflow (attempting to pop when the stack is empty). Therefore, we declare exceptions named Overflow and Underflow in the specification of Char__Stacks (see Example 15, page 46).

Example 15: The specification of the Char__Stacks package with exceptions added

     package Char_Stacks is

          type Char_Stack is private;

          procedure Push(S: in out Char_Stack; X: Character);
               -- pushes X onto stack S; raises Overflow if S is full

          procedure Pop(S: in out Char_Stack; X: out Character);
               -- stores the top element of S into X, then pops S
               -- raises Underflow if S is empty

          function Is_Empty(S: Char_Stack) return Boolean;
               -- returns True if S is empty, False otherwise

          Overflow, Underflow: exception;

     private
          Stack_Size: constant := 100;
          type Array_Type is array (1. .Stack_Size) of Character;
          type Char_Stack is
               record
                    Stack_Array:Array_Type;
                    Top_Of_Stack: Integer range 0. .Stack_Size := 0;
               end record;
     end Char_Stacks;

When Push detects that the stack is full, it raises the Overflow exception, using the Ada raise statement. Similarly, when Pop detects that the stack is empty, it raises the Underflow exception (see Example 16, page 48).

Example 16: The body of the Char__Stacks package with exceptions added

     package body Char_Stacks is

          procedure Push(S: in out Char_Stack; X: Character) is
          begin
               if S.Top_Of_Stack = Stack_Size then
                    raise Overflow;
               end if;
               S.Top_Of_Stack := S.Top_Of_Stack + 1;
               S.Stack_Array(S.Top_Of_Stack) := X;
          end Push;

          procedure Pop(S: in out Char_Stack; X : out Character) is
          begin
               if S.Top_Of_Stack = 0 then
                    raise Overflow;
               end if;
               X := S.Stack_Array(S.Top_of_Stack);
               S.Top_of_Stack := S.Top_of_Stack - 1;
          end Pop;

          function Is_Empty(S: Char_Stack) return Boolean is
          begin
               return S.Top_Of_Stack = 0;
          end Is_empty;

     end Char_Stacks;


Clients of Char__Stacks can now provide handlers for the Overflow and Underflow exceptions. Example 17, page 48, shows Reverse__String modified to handle the Overflow exception. (If the stack overflows, Reverse__String simply ignores the remaining input. Notice the use of the Ada null statement-- "dummy" statement that has no effect.)

Example 17: A program that uses the Char__Stacks package to reverse a string (With exception handling added)

   with Text_IO, Char_Stacks;
   use Text_IO, Char_Stacks;
   procedure Reverse_String is
      S: Char_Stack;
      Ch: Character;
   begin
      Put("Enter sr=tring to be reversed: ");
      begin
         while not End_of_Line loop
            Get(Ch);
            Push(S, Ch);
         end loop;
      exception
         when Overflow => null;    -- ignore overflow
      end;
      Skip_Line;

               Put("The reversal is: ");
      while not Is_Empty(S) loop
         Pop(S, Ch);
         Put(Ch);
      end loop;
      New_Line;
   end Reverse_String;

Generics

The Char__Stacks package is fine if we need only stacks whose elements are characters. What if we need stacks whose elements are integers, or real numbers, or records? One possibility would be to make a copy of the Char__Stacks package, replacing each occurrence of Character by some other type. This solution is not completely satisfactory. Not only is it a lot of work but it would also cause problems if we ever needed to change the package (to add additional subprograms, say, or to fix a bug, or to change the way the stack is represented): we would have to track down every copy of the package and change each one individually.

The designers of Ada anticipated this problem and provided a special feature to solve it. When we write a package, Ada allows us to omit certain information; we supply the missing information later, when the package is used. In the case of the Char__Stacks package, we would like to omit the type of the stack elements; this would allow us to write a completely general stack package that could be used with elements of any type. Such a package is said to be generic.

Validation: Ada's Greatest Strength Or Weakest Link

By James Stewart

James Stewart is general manager of R.R. Software Inc. and has participated in the Ada field for more than six years. He holds de grees from the University of Texas and the University of Wisconsin

Among the stated design goals for the Ada language were a desire: to reduce software development and maintenance costs, to provide portability of both software and programmers, and to encourage sound software engineering principles. To achieve these goals, Ada proponents established a set of validation tests that are supposed to ensure that compilers comply with a standard as well as to prevent the introduction of Ada subsets, supersets, or dialects.

In theory, the validation process is a good way to achieve these goals and there's no reason why in practice, validation shouldn't be straight-forward to implement. But there's often a big difference between theory and practice, and it's been the validation process itself that has scared off some compiler developers, thereby stifling acceptance of the language. To illustrate this point, what follows is a description of the experiences of one developer who wasn't scared by validation. -- eds.

Ada Validation: The Diary of a Vendor

At R.R. Software, we have conducted almost a dozen validations, both for our own compilers and for those we've made for other companies. This description of the validation process is tendered with admiration and respect for the participants, whom we trust can laugh as well as they work.

Getting Started

First, get an Ada compiler. Although that may sound easy, it takes the average developer more than 16 person years and two million dollars to accomplish.

Next, get the current validation suite from the government. Unless you know where to go, this could be almost as difficult as getting a compiler. The place to start is the Ada Joint Program Office (MPO) in Washington D.C. which will direct you to the Ada Validation Facilities (AVF). The AVF will send you the current Ada Compiler Validation Capability (ACVC) if you send the ACVC the appropriate media for your computer and indicate that you are planning to validate.

Now you're ready for the real fun...

Scheduling a Validation

To validate an Ada compiler, you must contract with your AVF. As it is working for the government, you should be prepared to wait ... and wait . . and wait. Prior to scheduling your validation, you should do two things: test your compiler against the ACVC and make sure the ACVC you have will still be in effect when you want to validate. We will assume that your compiler passes all the applicable tests (this is an all-or-nothing test). As the ACVC gets revised each year, you have to be careful to ensure you're testing with the current model (ACVC 1.10, currently).

If you have gotten this far prepare your letter to the AVF for a contract to validate; it should indicate what computer environments you are using, what compiler version you are testing, and what validation suite you expect to validate under. You should also indicate when you will send the prevalidation results and what date you want for the on-site validation. Last, you should give the AVF an estimate of how long you expect the actual validation to take (you should have a rough idea from running the tests for prevalidation).

Contracting for Validation

Having sent the AVF your letter, you will receive a letter confirming the subject matter of your letter. You will also receive a fairly short and simple government contract that details the actions of both parties, when the prevalidation and validation will occur, and what you must pay for this process. The cost of validation depends on several factors: the speed of your environment, the number of validations for which you contract, the number of prevalidations you desire (this lowers your risk at validation but is costly) as well as a plethora of more subtle factors. The import ant point to remember is that you will pay the indicated cost prior to commencing the validation process; be prepared to write the check if you're in a hurry!

The Prevalidation Process

You should already have tested your compiler against the ACVC in-house; now you need to submit the results in printed listings and media form, to the AVF. It will check the results and inform you when it thinks there are any problems. If you disagree, the question will be submitted to the Ada Validation Office (AVO), which decides the issue. This process can run anywhere from 45 days to three months, so grab a good book to read during this period. If you have done the job correctly, this will be a relatively quick and easy process.

The Actual Validation

The appointed day arrives and your validation team shows up, ready to test the compiler. The people on the team; will need a room to grade the tests after completion. They'll load the ACVC and start it running; from that point on, you will follow their directions and interact with them at least twice each day. Our own experiences with the Wright-Pattterson AVF have always been very pleasant; the pe6ple on the team have always been very helpful and professional, which is quite a relief during a tense period such as validation. When the process is completed, they go over the final Validation Summary Report (VSR) with you, telling you that the results are subject to review and sign-off by the AVO and AJPO. Therefore you can look forward to another wait for the actual certificates, usually six to eight weeks.

Post-Validation

You now have the final VSR and your certificates, so you think it's all over until you

look at the expiration date on the certificates to see that this will only last a year. This three to six month process is a yearly event with a tougher test each time.


Example 18, on page 51, shows the specification of a generic Stacks package. The word generic at the beginning identifies Stacks as a generic package. The next line specifies that the type Element will be supplied later, when the package is used. (Element is not an ordinary private type; it is a generic ape parameter. The words is private simply mean that Stacks must treat Element as a private type; it cannot assume that Element is a record type, for example.)

Example 18: The specification of generic Stacks package

     generic
          type Element is private;
     package Stacks is

          type Stack is private;

          procedure Push(S: is out Stack; X: Element);
               -- pushes X onto stack S; raises Overflow if S is full

          procedure Pop(S: in out Stack; X: out Element);
               -- stores the top element of S into X, then pops S
               -- raises Underflow if S is empty

          function Is_Empty(S: Stack) return Boolean;
               -- returns True if S is empty, False otherwise

          Overflow, Underflow: exception;

     private
          Stack_Size: constant :=100;
          type Array_Type is array (1. .Stack_Size) of Element;
          type Stack is
               record
                    Stack_Array: Array_Type;
                    Top_Of_Stack: Integer range 0. .Stack_Size := 0;
               end record;
     end Stacks;


The body of Stacks is identical to the body of Char__Stacks (Example 16), except that Stack replaces Char__Stack and Element replaces Character.

A generic package is not an ordinary package; it is a template from which packages can be created by a process called instantiation. Instantiating a generic package requires filling in the blanks that were left when the package was written. Before it can use the entities in the Stacks package, a client must first instantiate the package by specifying what Element is.

Example 19, page 51, shows how this would be done in the Reverse string program. Reverse__String first instantiates the generic Stacks package by specifying that Character be substituted for Element; the resulting package is given the name Char__Stacks (any name will do). The use clause on the following line allows Reverse__String to access Push, Pop, and Is__Empty directly, without having to mention Char__Stacks each time.

Example 19: A program that uses the generic Stacks package to reverse a string

     with Text_IO, Stacks;
     use Text_IO;
     procedure Reverse_String is

          package Char_Stacks is new Stacks(Cahracter);
          use Char_Stacks;

          S: Stack;
          Ch: Character;

     begin
          Put ("Enter string to be reverse: ");
          begin
               while not End_Of_Line loop
                    Get(Ch);
                    Push(S, Ch);
               end loop;
          exception
               when Overflow => null;
          end;
          Skip_line;

          Put("The reversal is: ');
          while not Is_Empty(S) loop;
               Pop(S, Ch);
               Put(Ch);
          end loop;
          New_Line;
     end Reverse_String;

You have seen instantiation of a generic package before. Examples 2 and 5 contain the following lines:

package Int__IO is
                    new Integer__IO(Integer);
use Int__IO;

The purpose of these lines is to instantiate the generic package Integer__IO, specifying that the procedures in Integer__IO will be used to read and write Integer values (as opposed to Short__Integer or Long__Integer values). The name of the instantiated package is chosen by the programmer; I use the name Int__IO. The use clause that follows makes the names of the procedures in Int__IO directly visible.

Ada's Problems

Ada is not the ultimate programming language. Like every other programming language, it has both advantages and disadvantages. I've discussed some of Ada's attractive features; now let's take a look at its problems. Some of Ada's powerful features have hidden disadvantages. Exception handling, for example, may mask errors; unioreseen errors may be handled without the programmer's knowledge. Overloading and use of default parameters can make programs hard to read.

Problems of compile- and run-time efficiency have plagued Ada since the begiioning. (A couple of years ago I heard that a well-known Ada compiler took 90 minutes to compile a 36-line program, then took another 45 minutes to link. And this was on a VAX 11/780 with no other jobs running!) Current compilers exhibit decent compilation speed, although few are likely to challenge Turbo Pascal. Slow compilation can largely be attributed to several of Ada's more complex features, including overloading and generics. Because of these features, Ada compilers will probably always be slower than compilers for Pascal, C, and other popular languages.

The below-par run-time performance of Ada programs is due to two factors. One is the extensive checking that Ada requires; although much of this checking can be done during compilation, a substantial part must be postponed until the program is run. Turning off these checks improves execution speed but may hide bugs and cause timing problems. Another factor is the stringent requirements of Ada validation (see the sidebar on page 45), which encourage compiler developers to devote substantial effort to validation at the expense of code generation and optimization.

Ada can be a deceptive language--its true complexity is not always apparent to beginners. Because of Ada's surface similarity to Pascal, programmers quickly progress to the stage of being able to read Ada programs. Writing Ada programs of small-to-moderate size is harder, but should not present too great an obstacle to most programmers. Getting to this stage may give the programmer a false sense of security, however. True mastery of Ada is extremely difficult because of the size of the language, the number of language features (some only rarely used), and the complex ways in which they interact. Few programmers will ever understand Ada at the level they understand smaller languages such as Pascal or C. One of the dangers of Ada is that project managers, misled by Ada's similarity to Pascal, will assume that Ada is not much harder to master than Pascal. Once committed to Ada, however, they discover that no one on the staff really understands it well enough to answer hard questions.

The complexity of Ada runs counter to the thinking of language designers who believe that programming languages should be kept small. Niklaus Wirth, the creator of Pascal and Modula-2, is in this group. Modula-2 appeared at about the same time as Ada. Although it is suitable for the same range of applications as Ada, Modula-2 is a simpler language. It contains several Ada-like features, including packages (modules in Modula-2), private types (opaque types in Modula-2), tasking (implemented using coroutines in Modula-2), and low-level access to the underlying machine. Modula-2, however, lacks some of Ada's more sophisticated (but expensive to implement) features, such as overloading, exceptions and generics. In short, Modula-2 captures the essence of Ada while avoiding Ada's overwhelming complexity.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.