Rationale for Ada 2005

John Barnes
Contents   Index   References   Search   Previous   Next 

4.5 Limited types and return statements

The general idea of a limited type is to restrict the operations that a user can do on the type to just those provided by the author of the type and in particular to prevent the user from doing assignment and thus making copies of objects of the type.
However, limited types have always been a problem. In Ada 83 the concept of limitedness was confused with that of private types. Thus in Ada 83 we only had limited private types (although task types were inherently limited).
Ada 95 brought significant improvement by two changes. It allowed limitedness to be separated from privateness. It also allowed the redefinition of equality for all types whereas Ada 83 forbade this for limited types. In Ada 95, the key property of a limited type is that assignment is not predefined and cannot be defined (equality is not predefined either but it can be defined). The general idea of course is that there are some types for which it would be wrong for the user to be able to make copies of objects. This particularly applies to types involved in resource control and types implemented using access types.
However, although Ada 95 greatly improved the situation regarding limited types, nevertheless two major difficulties have remained. One concerns the initialization of objects and the other concerns the results of functions.
The first problem is that Ada 95 treats initialization as a process of assigning the initial value to the object concerned (hence the use of := unlike some Algol based languages which use = for initialization and := for assignment). And since initialization is treated as assignment it is forbidden for limited types. This means that we cannot initialize objects of a limited type nor can we declare constants of a limited type. We cannot declare constants because they have to be initialized and yet initialization is forbidden. This is more annoying in Ada 95 since we can make a type limited but not private.
The following example was discussed in the Introduction 
type T is limited
   record
      A: Integer;
      B: Boolean;
      C: Float;
   end record;
Note that this type is explicitly limited (but not private) but its components are not limited. If we declare an object of type T in Ada 95 then we have to initialize the components (by assigning to them) individually thus 
   X: T;
begin
   X.A := 10;  X.B := True;  X.C := 45.7;
Not only is this annoying but it is prone to errors as well. If we add a further component D to the type T then we might forget to initialize it. One of the advantages of aggregates is that we have to supply all the components which automatically provides full coverage analysis.
This problem did not arise in Ada 83 because we could not make a type limited without making it also private and so the individual components were not visible anyway.
Ada 2005 overcomes the difficulty by stating that initialization by an aggregate is not actually assignment even though depicted by the same symbol. This permits
   X: T := (A => 10,  B => True,  C => 45.7);
We should think of the individual components as being initialized individually in situ – an actual aggregated value is not created and then assigned.
The reader might recall that the same thing happens when an aggregate is used to initialize a controlled type; this was not as Ada 95 was originally defined but it was corrected in AI-83 and consolidated in the 2001 Corrigendum [2].
We can now declare a constant of a limited type as expected 
   X: constant T := (A => 10,  B => True,  C => 45.7);
Limited aggregates can be used in a number of other contexts as well 
as the default expression in a component declaration,
so if we nest the type T inside some other type (which itself then is always limited – it could be explicitly limited but there is a general rule that a type is implicitly limited if it has a limited component) we might have 
type Twrapper is
   record
      Tcomp: T := (0, False, 0.0);
   end record;
as an expression in a record aggregate,
so again using the type Twrapper as in
XT: Twrapper := (Tcomp => (1, True, 1.0));
as an expression in an array aggregate similarly,
so we might have 
type Tarr is array (1 .. 5) of T;
Xarr: Tarr := (1 .. 5 => (2, True, 2.0));
as the expression for the ancestor part of an extension aggregate,
so if TT were tagged as in 
type TT is tagged limited
   record
      A: Integer;
      B: Boolean;
      C: Float;
   end record;
type TTplus is new TT with
   record
      D: Integer;
   end record;
...
XTT: TTplus := (TT'(1, True, 1.0) with 2);
as the expression in an initialized allocator,
so we might have 
type T_Ptr is access T;
XT_Ptr: T_Ptr;
...
XT_Ptr := new T'(3, False, 3.0);
as the actual parameter for a subprogram parameter of a limited type of mode in 
procedure P(X: in T);
...
P((4, True, 4.0));
similarly as the default expression for a parameter 
procedure P(X: in T := (4, True, 4.0));
as the result in a return statement 
function F( ... ) return T is
begin
   ...
   return (5, False, 5.0);
end F;
this really concerns the other major change to limited types which we shall return to in a moment.
as the actual parameter for a generic formal limited object parameter of mode in, 
generic
   FT: in T;
package P is ...
...
package Q is new P(FT => (7, True, 7.0));
The last example is interesting. Limited generic parameters were not allowed in Ada 95 at all because there was no way of passing an actual parameter because the generic parameter mechanism for an in parameter is considered to be assignment. But now the actual parameter can be passed as an aggregate. An aggregate can also be used as a default value for the parameter thus 
generic
   FT: in T := (0, False, 0.0);
package P is ...
Remember that there is a difference between subprogram and generic parameters. Subprogram parameters were always allowed to be of limited types since they are mostly implemented by reference and no copying happens anyway. The only exception to this is with limited private types where the full type is an elementary type.
The change in Ada 2005 is that an aggregate can be used as the actual parameter in the case of a subprogram parameter of mode in whereas that was not possible in Ada 95.
Sometimes a limited type has components where an initial value cannot be given as in 
protected type Semaphore is ... ;
type PT is
   record
      Guard: Semaphore;
      Count: Integer;
      Finished: Boolean := False;
   end record;
Since a protected type is inherently limited the type PT is also limited because a type with a limited component is itself limited. Although we cannot give an explicit initial value for a Semaphore, we would still like to use an aggregate to get the coverage check. In such cases we can use the box symbol <> as described in the previous section to mean use the default value for the type (if any). So we can write 
X: PT := (Guard => <>, Count => 0, Finished => <>);
The major rule that must always be obeyed is that values of limited types can never be copied. Consider nested limited types 
type Inner is limited
   record
      L: Integer;
      M: Float;
   end record;
type Outer is limited
   record
      X: Inner;
      Y: Integer;
end record;
If we declare an object of type Inner 
An_Inner: Inner := (L => 2, M => 2.0);
then we could not use An_Inner in an aggregate of type Outer
An_Outer: Outer := (X => An_Inner, Y => 3);     -- illegal
This is illegal because we would be copying the value. But we can use a nested aggregate as mentioned earlier 
An_Outer: Outer := (X => (2, 2.0), Y => 3);
The other major change to limited types concerns returning values from functions.
We have seen that the ability to initialize an object of a limited type with an aggregate solves the problem of giving an initial value to a limited type provided that the type is not private.
Ada 2005 introduces a new approach to returning the results from functions which can be used to solve this and other problems.
We will first consider the case of a type that is limited such as 
type T is limited
   record
      A: Integer;
      B: Boolean;
      C: Float;
   end record;
We can declare a function that returns a value of type T provided that the return does not involve any copying. For example we could have 
function Init(X: Integer; Y: Boolean; Z: Float) return T is
begin
   return (X, Y, Z);
end Init;
This function builds the aggregate in place in the return expression and delivers it to the location specified where the function is called. Such a function can be called from precisely those places listed above where an aggregate can be used to build a limited value in place. For example 
V: T := Init(2, True, 3.0);
So the function itself builds the value in the variable V when constructing the returned value. Hence the address of V is passed to the function as a sort of hidden parameter.
Of course if T is not private then this achieves no more than simply writing 
V: T := (2, True, 3.0);
But the function Init can be used even if the type is private. It is in effect a constructor function for the type. Moreover, the function Init could be used to do some general calculation with the parameters before delivering the final value and this brings considerable flexibility.
We noted that such a function can be called in all the places where an aggregate can be used and this includes in a return expression of a similar function or even itself 
function Init_True(X: Integer; Z: Float) return T is
begin
   return Init(X, True, Z);
end Init_True;
It could also be used within an aggregate. Suppose we have a function to return a value of the limited type Inner thus 
function Make_Inner(X: Integer; Y: Float) return Inner is
begin
   return (X, Y);
end Make_Inner;
then not only could we use it to initialize an object of type Inner but we could use it in a declaration of an object of type Outer thus 
An_Inner: Inner := Make_Inner(2, 2.0);
An_Outer: Outer := (X => Make_Inner(2, 2.0), Y => 3);
In the latter case the address of the component of An_Outer is passed as the hidden parameter to the function Make_Inner.
Being able to use a function in this way provides much flexibility but sometimes even more flexibility is required. A new form of return statement, the extended return statement, permits the final returned object to be declared and then manipulated in a general way before finally returning from the function.
The basic structure is 
function Make( ... ) return T is
begin
   ...
   return R: T do    -- declare R to be returned
      ...    -- here we can manipulate R in the usual way
      ...    -- in a sequence of statements
   end return;
end Make;
The general idea is that the object R is declared and can then be manipulated in an arbitrary way before being finally returned. Note the use of the reserved word do to introduce the statements in much the same way as in an accept statement. The sequence ends with end return and at this point the function passes control back to where it was called. Note that if the function had been called in a construction such as the initialization of an object X of a limited type T thus 
X: T := Make( ... );
then the variable R inside the function is actually the variable X being initialized. In other words the address of X is passed as a hidden parameter to the function Make in order to create the space for R. No copying is therefore ever performed.
The sequence of statements could have an exception handler 
   return R: T do
      ...      -- statements
   exception
      ...      -- handlers
   end return;
If we need local variables within an extended return statement then we can declare an inner block in the usual way 
   return R: T do
      declare
         ...    -- local declarations
      begin
         ...    -- statements
      end;
   end return;
The declaration of R could have an initial value 
   return R: T := Init( ... ) do
      ...
   end return;
Also, much as in an accept statement, the do ... end return part can be omitted, so we simply get 
   return R: T;
or
   return R: T := Init( ... );
which is handy if we just want to return the object with its default or explicit initial value.
Observe that extended return statements cannot be nested but could have simple return statements inside 
   return R: T := Init( ... ) do
      if ... then
          ...
          return;    -- result is R
      end if;
      ...
   end return;
Note that simple return statements inside an extended return statement do not have an expression since the result returned is the object R declared in the extended return statement itself.
Although extended return statements cannot be nested there could nevertheless be several in a function, perhaps in branches of an if statement or case statement. This would be quite likely in the case of a type with discriminants 
type Person(Sex: Gender) is ... ;
function F( ... ) return Person is
begin
   if ... then
      return R: Person(Sex => Male) do
         ...
      end return;
   else
      return R: Person(Sex => Female) do
         ...
      end return;
   end if;
end F;
This also illustrates the important point that although we introduced these extended return statements in the context of greater flexibility for limited types they can be used with any types at all such as the nonlimited type Person. The mechanism of passing a hidden parameter which is the address for the returned object of course only applies to limited types. In the case of nonlimited types, the result is simply delivered in the usual way.
We can also rename the result of a function call – even if it is limited.
The result type of a function can be constrained or unconstrained as in the case of the type Person but the actual object delivered must be of a definite subtype. For example suppose we have 
type UA is array (Integer range <>) of Float;
subtype CA is UA(1 .. 10);
Then the type UA is unconstrained but the subtype CA is constrained. We can use both with extended return statements.
In the constrained case the subtype in the extended return statement has to statically match (typically it will be the same textually but need not) thus 
function Make( ... ) return CA is
begin
   ...
   return R: UA(1 .. 10) do        -- statically matches
      ...
   end return;
end Make;
In the unconstrained case the result R has to be constrained either by its subtype or by its initial value. Thus 
function Make( ... ) return UA is
begin
   ...
   return R: UA(1 .. N) do
      ...
   end return;
end Make;
or
function Make( ... ) return UA is
begin
   ...
   return R: UA := (1 .. N => 0.0) do
      ...
   end return;
end Make;
The other important change to the result of functions which was discussed in Section 3.3 is that the result type can be of an anonymous access type. So we can write a function such as 
function Mate_Of(A: access Animal'Class) return access  Animal'Class;
The introduction of explicit access types for the result means that Ada 2005 is able to dispense with the notion of returning by reference.
This does, however, introduce a noticeable incompatibility between Ada 95 and Ada 2005. We might for example have a pool of slave tasks acting as servers. Individual slave tasks might be busy or idle. We might have a manager task which allocates slave tasks to different jobs. The manager might declare the tasks as an array 
Slaves: array (1 .. 10) of TT;    -- TT is some task type
and then have another array of properties of the tasks such as 
type Task_Data is
   record
      Active: Boolean := False;
      Job_Code: ... ;
   end record;
Slave_Data: array (1 .. 10) of Task_Data;
We now need a function to find an available slave. In Ada 95 we write 
function Get_Slave return TT is
begin
   ...    -- find index K of first idle slave
   return Slaves(K);    -- in Ada 95, not in Ada 2005
end Get_Slave;
This is not permitted in Ada 2005. If the result type is limited (as in this case) then the expression in the return statement has to be an aggregate or function call and not an object such as Slaves(K).
In Ada 2005 the function has to be rewritten to honestly return an access value referring to the task type rather than invoking the mysterious concept of returning by reference.
So we have to write
function Get_Slave return access TT is
begin
   ...    -- find index K of first idle slave
   return Slaves(K)'Access;    -- in Ada 2005
end Get_Slave;
and all the calls of Get_Slave have to be changed to correspond as well.
This is perhaps the most serious incompatibility between Ada 95 and Ada 2005. But then, at the end of the day, honesty is the best policy.

Contents   Index   References   Search   Previous   Next 
© 2005, 2006, 2007 John Barnes Informatics.
Sponsored in part by:
The Ada Resource Association and its member companies: ARA Members AdaCore Polyspace Technologies Praxis Critical Systems IBM Rational Sofcheck and   Ada-Europe:
Ada-Europe