This is part four (of five) of this article. Click here to return to part three, or click here to return to the start of the article.
Ada provides three different looping constructs.
The simplest loop in Ada is an unconditional loop.
loop -- Loop body goes here end loop;
This loop will continue forever, or until you execute an exit command within the loop. In Ada, the exit command terminates a loop. It does not terminate a program.
loop if condition then exit; end if; end loop;
This construct is so common that a shortened version is available.
loop exit when condition; end loop;
The simple loop combined with an exit statement is the most general form of looping in Ada, allowing you to perform your test at the top, bottom, or middle of each iteration.
The Ada while loop is like the while loop in most languages. It is a conditional loop that performs the condition test at the top of the loop.
while condition loop -- Loop body goes here end loop;
The Ada for loop iterates through an explicitly stated range of discrete values. As we discussed earlier, the concept of a range is very important in Ada. A range is specified by the notation lowest_value .. highest_value.
for variable in low_value .. high_value loop -- Loop body goes here end loop;
The name of a discrete type can be used in place of the value. In that case the loop will iterate over all values in the type, starting at the lowest value. Ranges always must be expressed as low_value..high_value. They can never be expressed in reverse order. You can iterate backwards through a range by adding the reserved word reverse.
for variable in reverse low_value .. high_value loop -- Loop body goes here end loop;
The control variable used in a for loop is only visible within the loop body. It is a read-only variable within the loop body, meaning that you cannot explicitly assign a value to the control variable. You do not declare the control variable before you use it. The compiler determines the data type for the range and creates the control variable to be an instance of that type.
The Ada if statement is fully blocked.
if condition then -- statement(s) end if; if condition then -- statement(s) else -- statement(s) end if;
The condition must always evaluate to a Boolean value (False or True).
I will compare this with the C if statement.
In C the if statement controls a conditional jump to the statement immediately following the if statement.
if (condition) /* statement */
In C the statement following the if may be a simple statement or a compound statement. The condition must evaluate to an integer. C has no boolean data type. C considers 0 to be false and any non-zero value to be true. The C if statement presents several opportunities for coding traps. The simplest problem occurs when you place a semicolon directly after the condition:
if (condition); /* statement */
This is always erroneous, but always legal C code. The semicolon acts as a null statement for the if condition. Execution will always flow to the statement following the semicolon because that statement is not controlled by the if statement. This problem never happens in Ada. If you put the extra semicolon in Ada code the compiler will produce an error message indicating incorrect syntax.
Another problem with the C if statement is how it handles nested if statement with else alternatives.
if (condition) if(condition 2) /* statement */ else /*statement */
In this example the else is associated with the nearest if , not with the outer if. This mistake cannot be made with Ada.
The Ada case statement is used to make one of many choices based upon an expression that evaluates to a discrete type. The Ada case statement must either explicitly deal with all possible values of the discrete type or it must contain an Others clause.
case expression is when choice list => sequence-of-statements when choice list => sequence-of-statements when others => sequence-of-statements end case;
The Others clause corresponds to the default option in a C switch statement. The Ada case statement does not suffer from fall through like the C switch statement. There is no need to explicitly break at the end of each case as must be done in C to prevent fall through.
type Directions is (North, South, East, West); Heading : Directions; case Heading is when North => Y := Y + 1; when South => Y := Y - 1; when East => X := X + 1; when West => X := X - 1; end case;
In this example the Others clause is not used because all possible values of the Directions type are explicitly handled.
Ada provides two kinds of subprograms, and makes a strong distinction between the two.
Procedures never return a value through a return statement, but may pass parameters out through their parameter list. Procedure parameters must have a passing mode. There are three passing modes: IN, OUT, and IN OUT. IN parameters are treated as constants within a subprogram. They may be read from but not written to. OUT parameters have no reliable initial value upon entering the procedure, but are expected to be written to. IN OUT parameters have an initial value and may be written to. If no mode is specified, the default mode is IN.
procedure Swap(Left, Right : IN OUT Integer) is Temp : Integer := Left; begin Left := Right; Right := Temp; end Swap;
Functions always return a value (except when the function raises an exception). Function parameters can only have the IN mode.
function Count_Letters(Item : String) return Natural is Count : Natural := 0; begin for I in Item'Range loop if Is_Letter(Item(I)) then Count := Count + 1; end if; end loop; return Count; end Count_Letters;
This function will count all the letters in any size string passed to it. The for loop determines the range to iterate through by inspecting the Range attribute of the string. Remember that a string is merely an array of characters.
Procedure parameter modes tell the compiler what you want to do with the parameters. They also document for the programmer what your intent is. Any parameter passed with an IN mode can cause no side effects in the calling code block. Any parameter passed with an OUT or IN OUT mode can be expected to change.
Actual parameters to a subprogram may be passed by position, as in C, or using named notation. It is even possible to mix the notations, with some restrictions. In general, most Ada programmers prefer to use named notation when there are multiple parameters. The Put procedure that writes an unsigned integer type to standard output is defined as:
procedure Put(Item : in Num; Width : in Field := Default_Width; Base : in Number_Base := Default_Base);
This procedure declares three formal parameters. The last two have default values. This means that you do not need to provide values for those parameters if you are happy with the defaults. When calling this procedure using all defaults you would use one of the following formats:
Put(Count); Put(Item => Count);
Both procedure calls above accomplish the same thing. The first one uses positional notation. The actual parameter Count is passed into the formal parameter Item. This is made explicit with the second call. Clearly, there is no great advantage to using named notation in this instance. If, however, you want to output your integer value in base 2 you must pass the value 2 to the Base formal parameter. In this case named notation is very useful.
Put(Item => Count, Base => 2); Put(Base => 2, Item => Count); Put(Count, Base => 2);
All three forms are allowed. The first form is generally preferred. Note that the use of named notation allows you to specify parameters in any order you choose. The last example shows a mixture of named and positional notation. This form has an important rule. All the positional parameters must be passed before any named parameter. Named notation is useful for code maintenance. It documents exactly what the original programmer meant to do without requiring the maintenance programmer to look up the subprogram definition to understand which parameters were receiving which value.
The primary structure used for encapsulation in Ada is called a package . Packages are used to group data and subprograms. Packages can be hierarchical, allowing a structured and extensible relationship for data and code. All the standard Ada libraries are defined within packages.
Package definitions usually consist of two parts, a package specification and a package body. The package specification declares all the public and private components in a package including data types, constants, functions, and procedures. Ada’s notion of private is actually closer to the C++ or Java notion of protected. Most package specifications require a corresponding package body. The exception is a package defining only constants. Package bodies contain the implementation of all the procedures, functions, protected types, and task types defined in a package specification. Package bodies can also declare and define data types, variables, constants, functions, and procedures not declared in the package specification. Anything declared withing a package body is visible only to functions, procedures, protected types, and task types defined in the same package body. Variables and constants declared in a package body correspond to private members of C++ or Java classes.
I will repeat the code shown earlier about operators. This code clearly demonstrates the use of a user defined package.
The first part is the package specification for a package named Operator_Example . This package defines two data types and the procedures and functions associated with those data types. No private data is defined in this package.
--------------------------------------------------------------- -- Ada operator examples --------------------------------------------------------------- package Operator_Examples is type Days is (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday); procedure Print_Message(For_Day : in Days); type Daily_Sales is array(Days) of Float; function Total(Sales : in Daily_Sales) return Float; function Geometric_Mean(Sales : in Daily_Sales) return Float; end Operator_Examples;
The second part is the package body for Operator_Example. The functions and procedures defined within this package need to call functions and procedures from other packages. The with clause is used to identify which packages are needed. The use clause is used to simplify the naming of those functions and procedures from other packages. For instance, without the clause
The Print_Message procedure below would need to be written as follows:
procedure Print_Message(For_Day : in Days) is begin if For_Day in Saturday..Sunday then Ada.Text_Io.Put_Line("Go Fishing!"); else Ada.Text_Io.Put_Line("Go To Work!"); end if; end Print_Message;
With clauses must be placed before the beginning of the package body or specification. They cannot be scattered throughout the code. A use clause may be placed in the same area as the with clause, or it may be placed in the declarative region of a code block. If it is placed in the declarative region of a code block its influence is restricted to that code block.
with Ada.Text_Io; use Ada.Text_Io; with Ada.Numerics.Elementary_Functions; use Ada.Numerics.Elementary_Functions; package body Operator_Examples is procedure Print_Message(For_Day : in Days) is begin if For_Day in Saturday..Sunday then Put_Line("Go Fishing!"); else Put_Line("Go To Work!"); end if; end Print_Message; function Total(Sales : in Daily_Sales) return Float is Sum : Float := 0.0; begin for I in Sales'range loop Sum := Sum + Sales(I); end loop; return Sum; end Total; function Geometric_Mean(Sales : in Daily_Sales) return Float is Product : Float := 1.0; begin for I in Sales'range loop Product := Product * Sales(I); end loop; return Product**(1.0 / Float(Sales'Length)); end Geometric_Mean; end Operator_Examples;
The last part is not a package at all, but a stand-alone procedure with no parameters. Ada requires you to create a stand-alone procedure with no parameters as the starting point of your program. This procedure needs to use types and subprograms from several packages. The appropriate with clauses are placed before the beginning of the procedure. Note that the with clause does not work like the preprocessor for C or C++. The contents of a package specification are not copied into the code at the point of a with clause. The C and C++ preprocessor will generate duplicate symbol names if a file is included into the same source file multiple times. Ada avoids that problem altogether.
--------------------------------------------------------------- -- The driver procedure to exercise the -- Operator_Examples package. --------------------------------------------------------------- with Ada.Text_Io; use Ada.Text_Io; with Ada.Float_Text_Io; use Ada.Float_Text_Io; with Operator_Examples; use Operator_Examples; procedure Operator_Exerciser is My_Sales : Daily_Sales; begin My_Sales(Monday) := 1234.56; My_Sales(Tuesday) := 1342.65; My_Sales(Wednesday) := 1432.55; My_Sales(Thursday) := 1332.44; My_Sales(Friday) := 2345.67; My_Sales(Saturday) := 2222.00; My_Sales(Sunday) := 1232.33; for Day in Days loop Put(Days'Image(Day) & ": "); Print_Message(Day); end loop; Put("Total sales: "); Put(Item => Total(My_Sales), Aft => 2, Exp => 0); New_Line; Put("Mean Sales: "); Put(Item => Geometric_Mean(My_Sales), Aft => 3, Exp => 0); New_Line; end Operator_Exerciser;
You can create a composite type containing multiple fields, each with possibly a different type, using a record. A record is very much like a struct in C.
type Address is record Street : Unbounded_String; City : Unbounded_String; Postal_Code : String(1..10); end record; My_Address : Address;
The Address type shown above has three fields. The variable My_Address is an instance of the Address type. Record fields are accessed using a simple dot (.) notation.
A.Street := To_Unbounded_String("1700 Pennsylvania Avenue"); A.City := To_Unbounded_String("Washington, D.C."); A.Postal_Code := "00000-0000";
An access type corresponds to a Java reference.
type Address_Ref is access Address; A_Ref := new Address; A_Ref.Street := To_Unbounded_String("17 Raven Road"); A_Ref.City := To_Unbounded_String("San Anselmo"); A_Ref.Postal_Code := "94960-1234";
Unlike C, there is no notational difference for accessing a record field directly or through an access value. To refer to the entire record accessed by an access value use the following notation:
Ada provides tools and constructs for extending types through inheritence. The most unusual feature about Ada’s approach to constructing classes and objects is that Ada has no class reserved word. In many object oriented languages the concept of a class is overloaded. It is both the unit of encapsulation and a type definition. Ada separates these two concepts. Packages are used for encapsulation. Tagged types are used to define extensible types. This allows some interesting flexibilities. It is entirely legal to define more than one tagged type in a package. This ability is most useful when you need to define mutually referencing (or circular referencing) types.
A tagged type definition is syntactically simply an extension of record type definition.
type Person is tagged record Name : String(1..20); Age : Natural; end record;
Such a type definition is performed in a package. Immediately following the type definition must be the procedures and functions comprising the primitive operations defined for this type. Primitive operations must have one of its parameters be of the tagged type, or for functions the return value can be an instance of the tagged type, allowing you to overload functions based upon their return type. This is a stark contrast to C++ and Java which do not allow you to overload based upon the return type of a function. The tagged type is extended by making a new tagged record based upon the original type.
type Employee is new Person with record Employee_Id : Positive; Salary : Float; end record;
Type Employee inherits all the fields and primitive operations of type Person.
This ends part four of this article. Click here to continue with part five, Concurrent Programming.