8.3. More Features

8.3.1. Function Pointers

In many situations, passing functions to other functions as parameters is the most suitable way to create flexible programs. Therefore, Ada95 supports type-safe function pointers or "access types for functions" in Ada parlance. Here is an implementation of the trapezoidal rule to compute the integral of a function.

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Float_Text_IO; use Ada.Float_Text_IO;

procedure Main is
   type Float_Function is access function (X: Float) return Float;

   function Integrate(F: Float_Function; A, B: Float) return Float is
      X: Float := A;
      Y: Float := 0.0;
      DeltaX: Float := 1.0e-5;
   begin
      loop
         Y := Y + DeltaX * (F(A) + F(B)) / 2.0;
         X := X + deltaX;
         if X >= B then exit; end if;
      end loop;
      return Y;
   end;

   function Square(X: Float) return Float is
   begin
      return X*X;
   end;
begin
   Put(Integrate(Square'Access, 0.0, 2.0));
   New_Line(1);
end;

As with other values, we obtain the pointer (or access) to a function using the Access attribute. We can call the access function as if it were a regular function (as long as it has parameters - otherwise we need to user F.all).

8.3.2. Generic Packages

Strong typing necessitates some mechanism to define types depending on other types. Ada lets us parametrize functions and packages with type, functions, values, and even other packages.

The following first example shows a generic Swap procedure using the Item type as a type parameter.

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

procedure Main is
   generic
      type Item is private;
   procedure Swap(X, Y: in out Item);

   procedure Swap(X, Y: in out Item) is
      T: Item;
   begin
      T := X; X := Y; Y := T;
   end;

   procedure Integer_Swap is new Swap(Integer);

   A: Integer := 55;
   B: Integer := 66;
begin
   Put(A); Put(B); New_Line(1);
   Integer_Swap(A, B);
   Put(A); Put(B); New_Line(1);
end;

The procedure becomes generic by placing the declaration of the generic parameters between the generic and procedure keywords. When using generic constructs, we always have to separate the declaration of the generic entity (here the procedure Swap) from its definition. In contrast to C++, we do not have to repeat the generic part in the definition.

To use the generic procedure, we have to instantiate it with a generic parameter. In our example, we instantiate the generic Swap procedure with the Integer type. The generic parameters are passed just like any other function or procedure parameters.

As mentioned above, generics can be used with almost every construct in Ada. Here is a slightly more complex example defining a generic package implementing a stack.

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

procedure Main is
   generic
      Max: Positive;
      type Item is private;
   package Stack is
      procedure Push(X: in Item);
      function Pop return Item;
      function Is_Empty return Boolean;
   end Stack;

   package body Stack is
      Data: array(0 .. Max) of Item;
      Top: Integer range 0 .. Max := 0;

      procedure Push(X: in Item) is
      begin
         Data(Top) := X;
         Top := Top + 1;
      end Push;

      function Pop return Item is
      begin
         Top := Top - 1;
         return Data(Top);
      end Pop;

      function Is_Empty return Boolean is
      begin
         return Top = 0;
      end Is_Empty;
   end Stack;

   package My_Stack is new Stack(10, Integer);
   use My_Stack;

begin
   Push(55);
   Push(66);

   while not Is_Empty loop
      Put(Pop); New_Line(1);
   end loop;
end;

The approach is the same: we have to split declaration and definition and put the generic part ahead of the declaration. The example also demonstrates the use of two different kinds of generic parameters: the positive integer Max and the (arbitrary) type Item.

The example becomes more useful if we define a stack type inside a generic package. This way we can use multiple stacks.

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

procedure Main is
   generic
      type Item is private;
      Max: Positive;
      Default: Item;
   package Stack_Of is
      type Stack is private;

      procedure Push(S: in out Stack; X: in Item);
      procedure Pop(S: in out Stack; X: out Item);
      function Is_Empty(S: in Stack) return Boolean;
   private
      type Stack_Data is array(0 .. Max) of Item;
      type Stack is
         record
            Top: Integer range 0 .. Max := 0;
            Data: Stack_Data := (others => Default);
         end record;
   end Stack_Of;

   package body Stack_Of is
      procedure Push(S: in out Stack; X: in Item) is
      begin
         S.Data(S.Top) := X;
         S.Top := S.Top + 1;
      end Push;

      procedure Pop(S: in out Stack; X: out Item) is
      begin
         S.Top := S.Top - 1;
         X := S.Data(S.Top);
      end Pop;

      function Is_Empty(S: in Stack) return Boolean is
      begin
         return S.Top = 0;
      end Is_Empty;

   end Stack_Of;

   package My_Stack is new Stack_Of(Integer, 10, 0);

   S: My_Stack.Stack;
   X: Integer;
begin
   My_Stack.Push(S, 55);
   My_Stack.Push(S, 66);

   while not My_Stack.Is_Empty(S) loop
      My_Stack.Pop(S, X);
      Put(X); New_Line(1);
   end loop;
end;

Once we have understood Ada's generic machinery, its application to more complex situations is straight forward.

8.3.3. Overflow

Ada was clearly designed with safety as a first priority (that is, for "mission critical" applications in the sense of "space mission"). As an example, all numerical computations are checked for overflows. The following program would run happily forever using C, but stops rather quickly in Ada once the limit of the integer range is exceeded.

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

procedure Int_Overflow is
   X: Integer := 1;
begin
   loop
      X := 2 * X;
      Put("X=");
      Put(X);
      New_Line(1);
   end loop;
end;

output:
X=          2
X=          4
X=          8
...
X=  268435456
X=  536870912
X= 1073741824

raised CONSTRAINT_ERROR : int_overflow.adb:8

Using the gnat compiler, we have to specify the -gnato option to experience the desired behavior, since gnat switches the overflow checking off by default.

8.3.4. Modular Types

Here is another example of Ada's nifty little features solving everyday programming problems: modular types. Who has not dealt with some cyclic integer type throughout his or her programming career coding the module arithmetic by hand? In Ada, we simply define the type as mod N where N is some positive integer.

with Ada.Text_IO; use Ada.Text_IO;

procedure Main is
   N: constant Integer := 3;
   type Ring is mod N;
   X: Ring := 0;

   package Ring_IO is new Modular_IO(Ring); use Ring_IO;
begin
   for I in 0 .. 10 loop
      Put(X);
      X := X + 1;
   end loop;
end;

The modular type Ring consists of the numbers zero to two. All computations, such as adding on in the loop, are performed modulo three. Like enumerations, modular types have their own generic input/output package Modular_IO which we instantiate here as Ring_IO in order to be able to print the value of the modular variable X.

The standard package Interfaces (called this way because it is used to interface with other languages such as C) contains a number of modular types where the modulus N is a power of two, for example Unsigned_8 with modulus 256. Together with these types, the package also provides shift and rotate functions.

with Ada.Text_IO; use Ada.Text_IO;
with Interfaces; use Interfaces;

procedure Main is
   X: Unsigned_8 := 1;

   package Unsigned_8_IO is new Modular_IO(Unsigned_8); use Unsigned_8_IO;
begin
   for I in 0 .. 10 loop
      Put(X);
      X := Rotate_Right(X, 1);
   end loop;
end;

output:    1 128  64  32  16   8   4   2   1 128  64

8.3.5. Parallelism

Most modern programming languages support parallel programming with some multithreading API and sometimes additional primitives for synchronization such as Java's synchronized keyword. Ada uses a different approach and models typical patterns of defining and controlling parallel activities directly in the language.

The first tool is the task construct which allows us to define an activity which is run in parallel to the instructions of a procedure.

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

procedure Main is
   task Sub;
   task body Sub is
   begin
      for I in 1 .. 5 loop
         Put("Sub:  "); Put(I); New_Line(1);
         delay 0.2;
      end loop;
   end Sub;
begin
   for I in 1 .. 5 loop
      Put("Main: "); Put(I); New_Line(1);
      delay 0.1;
   end loop;
end;

The main procedure counts from one to five with a delay of 0.1 seconds. The task Sub also counts from one to five, but with a delay of 0.2 seconds. The resulting output shows how the two activities are executed in parallel. The procedure stops after all threads are done, that is, the main thread is waiting until the sub task is finished.

output:
Main:           1
Sub:            1
Main:           2
Sub:            2
Main:           3
Main:           4
Sub:            3
Main:           5
Sub:            4
Sub:            5

A task allows us to run multiple activities in parallel, but to become useful they have to be able to communicate with each other. There are two way to accomplish this kind of communication in Ada: using shared data or sending messages.

Beginning with messages, we can define entry points which cause a task to wait until another task calls (sends a message to) the entry point. This mechanism is called a rendezvous. Apart from the different keywords (entry for the declaration and accept for the definition), an entry looks just like a procedure.

The following example defines the entry Wake_Up in the task Sub with a single integer argument. The task consists of a loop which processes Wake_Up messages until the argument is equal to zero.

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

procedure Main is
   task Sub is
      entry Wake_Up(I: Integer);
   end Sub;

   task body Sub is
      Stop: Boolean := False;
   begin
      while not Stop loop
         Put("Sub:  Wait"); New_Line(1);
         accept Wake_Up(I: Integer) do
            Put("Sub:  "); Put(I); New_Line(1);
            if I = 0 then
               Stop := True;
            end if;
         end Wake_Up;
      end loop;
      Put("Sub:  Stop"); New_Line(1);
   end Sub;
begin
   for I in reverse 0 .. 3 loop
      delay 0.1;
      Put("Main: Send"); New_Line(1);
      Sub.Wake_Up(I);
   end loop;
   Put("Main: Stop"); New_Line(1);
end;

The main procedure sends Wake_Up messages counting from three down to zero. The statement Sub.Wake_Up(I) submitting the message looks like a method call. Here is corresponding output:

Sub:  Wait
Main: Send
Sub:            3
Sub:  Wait
Main: Send
Sub:            2
Sub:  Wait
Main: Send
Sub:            1
Sub:  Wait
Main: Send
Sub:            0
Main: Stop
Sub:  Stop

Of course, we are not retricted to a single entry. Here is an example of a buffer containing a single value. It is implemented as a task with the two entries Set and Get.

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

procedure Main is
   task Buffer is
      entry Set(X: in Integer);
      entry Get(X: out Integer);
   end;

   task body Buffer is
      Value: Integer;
   begin
      loop
         accept Set(X: in Integer) do
            Put("Buffer.Set: "); Put(X); New_Line(1);
            Value := X;
         end Set;
         accept Get(X: out Integer) do
            Put("Buffer.Get: "); Put(Value); New_Line(1);
            X := Value;
         end Get;
      end loop;
   end Buffer;

   task Consumer;
   task body Consumer is
      I: Integer;
   begin
      loop
         Buffer.Get(I);
         Put("Consumer:   "); Put(I); New_Line(1);
      end loop;
   end Consumer;

begin
   for I in 0 .. 2 loop
      Buffer.Set(I);
      delay 1.0;
   end loop;
end;

The Consumer task continuously asks the buffer for the latest value and "consumes" it. The main procedure inserts the values zero to two into the buffer with a one second delay. Here is the resulting output:

Buffer.Set:           0
Buffer.Get:           0
Consumer:             0
Buffer.Set:           1
Buffer.Get:           1
Consumer:             1
Buffer.Set:           2
Buffer.Get:           2
Consumer:             2

Note how the buffer task and its entries make sure that the actions are carried out in the correct order.

In the previous example the consumer was fast and the producer (the main task) slow. The opposite case is often handled with a queue. In this situation, the task may receive both events (provided the queue is neither empty nor completely full). Ada supports this with the select statement and guarding conditions which can be associated with entries.

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

procedure Main is
   task Queue is
      entry Push(X: in Integer);
      entry Pull(X: out Integer);
   end;

   task body Queue is
      N: Integer := 10;
      In_Ptr, Out_Ptr, Count : Integer := 0;
      Values: array (Integer range 0 .. N - 1) of Integer;
   begin
      loop
         select
            when Count < N =>
               accept Push(X: in Integer) do
                  Values(In_Ptr) := X;
                  In_Ptr := (In_Ptr + 1) mod N; Count := Count + 1;
               end;
         or
            when Count > 0 =>
               accept Pull(X: out Integer) do
                  X := Values(Out_Ptr);
                  Out_Ptr := (Out_Ptr + 1) mod N; Count := Count - 1;
               end;
         end select;
      end loop;
   end Queue;

   task Consumer;
   task body Consumer is
      I: Integer;
   begin
      loop
         delay 1.0;
         Queue.Pull(I);
         Put("Consumer: "); Put(I); New_Line(1);
      end loop;
   end Consumer;

   task Producer;
   task body Producer is
   begin
      for I in 10 .. 12 loop
         delay 3.0;
         Put("Producer: "); Put(I); New_Line(1);
         Queue.Push(I);
      end loop;
   end Producer;

begin
   for I in 0 .. 2 loop
      Put("Main:     "); Put(I); New_Line(1);
      Queue.Push(I);
   end loop;
end;

To make things more interesting we added a second (slow) producer. This way we see some of the scenarios in the output:

Main:               0
Main:               1
Main:               2
Consumer:           0
Consumer:           1
Producer:          10
Consumer:           2
Consumer:          10
Producer:          11
Consumer:          11
Producer:          12
Consumer:          12

First, the fast main producer pushes the values zero to two on the queue. They are consumed by the consumer, while the slow producer starts pushing his values. In the end the consumer is faster than the producers.

For shared data access as the second way of communication between parallel tasks, Ada has so-called protected objects. Apart from the protected keyword they look like packages, but the data (which must be defined in the private section of the protected object's declaration) is protected from simultaneous access. The following example wraps a simple integer value in a protected object Shared_Data.

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

procedure Main is
   protected Shared_Data is
      function Get return Integer;
      procedure Set(New_Value: Integer);
   private
      Value: Integer := 0;
   end Shared_Data;

   protected body Shared_Data is
      function Get return Integer is
      begin
         return Value;
      end Get;

      procedure Set(New_Value: Integer) is
      begin
         Value := New_Value;
      end Set;
   end Shared_Data;
begin
   Shared_Data.Set(55);
   Put(Shared_Data.Get); New_Line(1);
end;

In many cases we would like to define not just a single protected object, but a type which we can use to create as man protected objects as we want to. This is achieved by adding the type keyword to the declaration of the protected object. Here is a variation of the previous example using such a protected type.

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

procedure Main is
   protected type Shared_Data is
      function Get return Integer;
      procedure Set(New_Value: Integer);
   private
      Value: Integer := 0;
   end Shared_Data;

   protected body Shared_Data is
      function Get return Integer is
      begin
         return Value;
      end Get;

      procedure Set(New_Value: Integer) is
      begin
         Value := New_Value;
      end Set;
   end Shared_Data;

   X: Shared_Data;
begin
   X.Set(55);
   Put(X.Get); New_Line(1);
end;

As you can imagine, protected types are useful for the typical semaphore patterns.