Here is the message to the (complex) world as seen by Ada.
with Ada.Text_IO; procedure Hello is begin Ada.Text_IO.Put_Line("Hello World"); end;
To run the program, put the source code in a file called hello.adb and call gnatmake.
ahohmann@kermit> gnatmake hello.adb gnatgcc -c hello.adb gnatbind -x hello.ali gnatlink hello.ali ahohmann@kermit> hello Hello World
What does this simple example tell us? Apparently, Ada has a package structure, and we have to tell the compiler which packages we would like to use. In this case, it is the standard library package Ada.Text_IO. Coming from a structural programming background, Ada does not insist on classes for everything. Procedure Put_Line, for example, is a direct member of the standard Text_IO package, and our own procedure Hello does not have any surrounding structure.
From this tiny example we can tell that Ada definitely does not belong to the C family, but in contrast to Eiffel we have to live with plenty of semicolons separating statements. As a matter of style, Ada uses the underscore character and capitalization of the first letter to separate words in identifiers. However, this is nothing but a style convention, since Ada is case insensitive (apart from string literals).
Since Ada is mainly aimed at technical applications, is should not be surprising that standard arithmetical expressions work as expected.
with Ada.Text_IO; with Ada.Float_Text_IO; procedure Test_Arithmetic1 is X, Y: Float; begin Y := 10.0; X := Y + 3.5 * 4.0 + 2.0 ** 3; Ada.Text_IO.Put("X="); Ada.Float_Text_IO.Put(X); Ada.Text_IO.New_Line(1); end; result: X= 3.20000E+01
What's new in this example? First, the Pascal-like declaration of the variables X and Y. All local variables of a procedure have to be declared in advance in the block between is and the begin. Second, Ada uses, again like Pascal, the proper assignment operator :=. Third, arithmetical expressions use standard infix operators and preferences.
We also use a new standard package for printing floating point number, Ada.Float_Text_IO. Since the standard library calls start getting tedious and hard to read, we should introduce the use directive which imports all the symbols of a package into the local namespace (just like C++ and C#).
with Ada.Text_IO; use Ada.Text_IO; with Ada.Float_Text_IO; use Ada.Float_Text_IO; procedure Test_Arithmetic2 is X, Y: Float; begin Y := 10.0; X := Y + 3.5 * 4.0 + 2.0 ** 3; Put("X="); Put(X); New_Line(1); end;
As we see, Ada supports overloading and picks the correct procedure depending on the argument type. If Java, C#, and Eiffel are called strongly typed languages, I'm tempted to call Ada a "very strongly typed" language. The constants in the example above, for example, have to be floating point constants or the program will not compile. If we use integer constants, we need to explicitly cast them to floating point numbers like in the following assignment.
procedure Test_Arithmetic is X: Float; begin X := Float(5); end;
We get another glimpse of Ada's very strong typing when looking at our first own type, a simple enumeration. While C's enumerations are just integers in disguise and Java abandoned enumerations alltogether, Ada fully supports them with type safety and a complete set of operations.
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; use Ada.Integer_Text_IO; procedure Enum_Test is type Color is (Red, Blue, Green); package Color_IO is new Enumeration_IO(Color); use Color_IO; begin Put("Color'Pos(Blue)="); Put(Color'Pos(Blue), 1); Put(" ("); Put(Blue); Put_Line(")"); end; output: Color'Pos(Blue)=1 (BLUE)
This example introduces a number of new features. We start with the definition of the enumeration type Color itself. Remember that identifiers are case insensitive, so that Blue, BLUE, and bLue are all the same value.
The next line is a first example of the instantiation of a generic package. A strongly typed language can hardly live without parametrized types (if only for type-safe collections), and generic packages are Ada's implementation of this concept. We have already used generic packages without thinking about it. The standard package Float_Text_IO, for example, is an instantiation of the generic package Float_IO. We could have defined it as
package Float_Text_IO is new Float_IO(Float);
Float_IO is a generic package defining input and output operations for any kind of floating point type. We will learn how to define our own generic types in Section 8.3.2>.
Similar to Float_IO, the generic Enumeration_IO package defines procedures for any kind of enumeration type. In the example, we instantiate it with our own enumeration type Color.
The next new feature is the expression Color'Pos(Blue). Ada defines a number of attributes for the entities (such as types, packages, array) occuring in the language. The syntax for accessing such a predefined attribute uses an apostrophe. One attribute of an enumeration type is the Pos function which return the integer position (starting at zero) of an enumeration value. Observe that, unlike C, the position is completely separated from the actual enumeration value.
We have already defined simple procedures without parameters in the examples above. Parameters are specified in Pascal manner with the parameter name followed by a colon and the parameter's type. A function returns a value whose type is is stated with the return keyword.
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; use Ada.Integer_Text_IO; procedure Main is function Times2(X: Integer) return Integer is begin return 2*X; end; begin Put("Times2(55)="); Put(Times2(55)); New_Line(1); end;
The example also demonstrates that fuctions can be defined within other functions or procedures. In general, Ada allows to definitions inside other definitions.
Multiple parameter declarations are separated with semicolons, and multiple parameters of the same type use a comma.
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; use Ada.Integer_Text_IO; procedure Main is function Add(A: Integer; B, C: Integer) return Integer is begin return A + B + C; end; begin Put("Add(4, 5, 6)="); Put(Add(4, 5, 6)); New_Line(1); end;
By default, a function or procedure can not modify the values passed to it. The arguments are passed by value (and not copied back). However, we may qualify a parameter using the in and out qualifiers to change this default behavior. Here is the Times2 function realized as a procedure modifying its argument.
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; use Ada.Integer_Text_IO; procedure Main is procedure Times2(X: in out Integer) is begin X := 2*X; end; X: Integer := 55; begin Times2(X); Put(X); New_Line(1); end;
Ada has all the control structures we expect from a modern procedural language. All use the same block structure with the end keyword following by the name of the control structure. There are two conditional statements, if and case.
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; use Ada.Integer_Text_IO; procedure Main is function Sign(X: Integer) return Integer is begin if X < 0 then return -1; elsif X > 0 then return 1; else return 0; end if; end; begin Put(Sign(123)); Put(Sign(-123)); New_Line(1); end;
The case statement allows us to uses ranges and alternatives to select the alternative actions.
with Ada.Text_IO; use Ada.Text_IO; procedure Main is type Day is (Mon, Tue, Wed, Thu, Fri, Sat, Sun); function WorkDay(D: Day) return Boolean is begin case D is when Mon .. Fri => return True; when Sat | Sun => return False; end case; end; begin if WorkDay(Mon) then Put("Monday is a work day"); end if; New_Line(1); end;
Alternatively, we could have used the others keyword to define the action for the remaining cases.
function WorkDay(D: Day) return Boolean is begin case D is when Mon .. Fri => return True; when others => return False; end case; end;
Loops come in three different flavors: plain loops with exit statements, while loops, and for loops over ranges of enumerated types. Here are three different ways to count from zero to nine.
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO; procedure Main is N: constant Integer := 10; I: Integer; begin I := 0; loop Put(I); I := I + 1; if I = N then exit; end if; end loop; I := 0; while I < N loop Put(I); I := I + 1; end loop; for I in 0 .. N - 1 loop Put(I); end loop; end;
The example also demonstrates how to define constants in Ada: just put the constant keyword in front of the type.
The for loop lets us go backwards as well, but we can not vary the step size. Note that the range is always specified in ascending order.
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO; procedure Main is N: constant Integer := 10; begin for I in reverse 1 .. N loop Put(I); end loop; end;
Ada's for loops are not restricted to integers. We can use any range of an enumerated type.
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO; procedure Main is type Day is (Mon, Tue, Wed, Thu, Fri, Sat, Sun); begin for I in Day loop Put(Day'Pos(I)); end loop; for I in Mon .. Fri loop Put(Day'Pos(I)); end loop; end;
The exit statement can also be used with the while and for loops, and it may refer to a label if we would like to exit not just the innermost loop. Just like the goto statement, we will conciously skip this part, since there are typically better solutions available.
While other strongly types languages offer a number of predefined integer and floating point types, Ada takes strong typing one step further and allows us to define any range as a new type. The statement
subtype Die is Integer range 1 .. 6;
defines the type consisting of the integer numbers one to six. The following program uses this type for a simple electronic die.
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO; with Ada.Numerics.Discrete_Random; procedure Test_Arithmetic5 is subtype Die is Integer range 1 .. 6; package Random_Die is new Ada.Numerics.Discrete_Random(Die); use Random_Die; D: Die; G: Generator; begin loop D := Random(G); Put(D); delay Duration(1); end loop; end;
Again, we make use of a generic standard package. We define the random number package for our new type by instantiating Discrete_Random. We then use the Generator type out of this package to declare the random number generator for our subtype. The Random function (also from the Random_Die package) is called in an infinite loop to generate new random numbers. The delay statement holds the program for one second.
Similarly, the floating point interval between zero and one can be defined as the subtype
subtype Chance is Float range 0.0 .. 1.0;
When using such a subtype, leaving its range will cause a constraint exception.
procedure Test_Arithmetic3 is subtype Chance is Float range 0.0 .. 1.0; X: Chance; begin X := 1.5; end; result: raised CONSTRAINT_ERROR : test_arithmetic4.adb:5
Another example of Ada's careful treatment of types are arrays. An array can be seen as a map of a fixed number of indexes to values. Each index is taken from a finite, enumerated set. In other words, an array maps the cross product of some finite enumerated types to the value type. In the simplest case we can define a vector, that is, a one dimensional array whose indexes are taken from an integer range.
with Ada.Text_IO; use Ada.Text_IO; with Ada.Float_Text_IO; use Ada.Float_Text_IO; procedure Main is A: array (Integer range 0 .. 2) of Float; begin A(2) := 55.0; Put(A(2)); New_Line(1); end;
Instead of the integer range, we can also use our own enumeration type.
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; use Ada.Integer_Text_IO; procedure Main is type Color is (Red, Blue, Green); A: array (Color) of Integer; begin for I in Color loop A(I) := 0; end loop; A(Red) := 37; for I in A'Range(1) loop Put(A(I)); end loop; New_Line(1); end;
This example also demonstrates how we can extract the range information from the array itself. The Range(1) attribute gives us the range of the first dimension.
We can initialize the array while defining it using Ada's flexible array "aggregates" which can be used to set individual elements, alternatives, ranges, or all remaining elements all in one expression.
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; use Ada.Integer_Text_IO; procedure Main is type Day is (Mon, Tue, Wed, Thu, Fri, Sat, Sun); A: array (Day) of Integer := ( Tue => 1, Mon | Wed => 2, Thu .. Sat => 3, others => 4); begin for I in A'Range(1) loop Put(A(I)); end loop; New_Line(1); end;
Multidimensional arrays work the same way with full support for aggregates.
with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; use Ada.Integer_Text_IO; procedure Main is type Day is (Mon, Tue, Wed, Thu, Fri, Sat, Sun); type Schedule is array (Day, 0 .. 23) of Boolean; Busy: Schedule := (Mon .. Fri => (8 .. 16 => True, others => False), others => (others => False)); package Day_IO is new Enumeration_IO(Day); use Day_IO; begin for D in Busy'Range(1) loop Put(D); Put(":"); for H in Busy'Range(2) loop Put(" "); if (Busy(D, H)) then Put("T"); else Put("F"); end if; end loop; New_Line(1); end loop; end;
Here we define a Schedule type mapping the days of the week and the 24 hours to the boolean type. We then declare the Busy variable of this type and initialize it with True during the working hours of the work days.
Ada's typesafe variant of pointers are access types. Like in many other languages, the new operator (also called "allocator") creates an object on the heap and returns a reference to it, that is, an access type value which allows us to access the allocated object.
As one of Ada's unique twists, we dereference a pointer with the special all attribute.
A record consists of named fields called "components" in Ada. Like in most languages we cover in this book, the components are referenced using a period between the record variable and the component name.
with Ada.Float_Text_IO; use Ada.Float_Text_IO; procedure Main is type Point is record X: Float; Y: Float; end record; P: Point := (0.0, 0.0); begin P.Y := 1.5; Put(P.X); Put(P.Y); end;
Record initializer can become as complex as the array aggregates in the last section. In the previous example we have used positional values, but we can as easily use the component names (X => 0.0, Y => 0.0), multiple alternatives (X | Y => 0.0), the others keyword (others => 0.0), or even a combination of positional and named initializer (0.0, Y => 0.0) to achieve the same effect.
Here is the Ada version of the simplistic Person structure.
with Ada.Text_IO; use Ada.Text_IO; procedure Main is type Ref_String is access String; type Person is record Name: Ref_String; Age: Integer := 0; end record; P: Person; begin P.Name := new String'("Homer"); P.Age := 55; Put_Line("Name=" & P.Name.all); end;
The first step towards object orientation is a the ability to extends record types. In order to do so the base type has to be "tagged". For now, this only means adding the tagged keyword to the type definition. Under the hood, it implicitly adds type information to the record which can be used by Ada at runtime to determine the type of a record.
The main organizational unit of Ada is a package. A package combines types and functions which belong together just like attributes and methods of a class in a "pure" object-oriented language. Similar to C++ and Objective C, Ada splits the package definition into two parts: package declaration and package body. The package declaration is used by the compiler to support separate compilation units with well-defined interfaces (and enough information to link the separately compiled units to an executable).
The package version of "Hello World" consists of three files (this example is taken from gnat's user guide): The first file, greetings.ads ("ads" like "Ada Specification") contains the package specification declaring the package Greetings with two procedures Hello and Goodbye
package Greetings is procedure Hello; procedure Goodbye; end Greetings;
The package body contained in greetings.adb ("adb" like "Ada Body") implements these procedures.
with Text_IO; use Text_IO; package body Greetings is procedure Hello is begin Put_Line ("Hello WORLD!"); end Hello; procedure Goodbye is begin Put_Line ("Goodbye WORLD!"); end Goodbye; end Greetings;
Finally, we need a main program using our new package. The filename helloworld.adb corresponds to the name of the main procedure.
with Greetings; procedure HelloWorld is begin Greetings.Hello; Greetings.Goodbye; end HelloWorld;
When compiling the main program using gnatmake helloworld.adb, the compiler sees the import statement with Greetings and automatically looks for this package. It finds the specification and body in the local directory, compiles it, and links all the components together to obtain the helloworld application.