Rationale for Ada 2012

John Barnes
Contents   Index   References   Search   Previous   Next 

1.3.2 Overview: Expressions

Those whose first language was Algol 60 or Algol 68 or who have had the misfortune to dabble in horrid languages such as C will have been surprised that a language of the richness of Ada does not have conditional expressions. Well, the good news is that Ada 2012 has at last introduced conditional expressions which take two forms, if expressions and case expressions.
The reason that Ada did not originally have conditional expressions is probably that there was a strong desire to avoid any confusion between statements and expressions. We know that many errors in C arise because assignments can be used as expressions. But the real problem with C is that it also treats Booleans as integers, and confuses equality and assignment. It is this combination of fluid styles that causes problems. But just introducing conditional expressions does not of itself introduce difficulties if the syntax is clear and unambiguous.
If expressions in Ada 2012 take the form as shown by the following statements:
S := (if N > 0 then +1 else 0);
Put(if N = 0 then "none" elsif N = 1 then "one" else "lots");
Note that there is no need for end if and indeed it is not permitted. Remember that end if is vital for good structuring of if statements because there can be more than one statement in each branch. This does not arise with if expressions so end if is unnecessary and moreover would be heavy as a closing bracket. However, there is a rule that an if expression must always be enclosed in parentheses. Thus we cannot write
X := if L > 0 then M else N + 1;    -- illegal
because there would be confusion between
X := (if L > 0 then M else N) + 1;    -- and
X := (if L > 0 then M else (N + 1));
The parentheses around N+1 are not necessary in the last line above but added to clarify the point.
However, if the context already provides parentheses then additional ones are unnecessary. Thus an if expression as a single parameter does not need double parentheses.
It is clear that if expressions will have many uses. However, the impetus for providing them in Ada 2012 was stimulated by the introduction of aspects of the form
Pre => expression
There will be many occasions when preconditions have a conditional form and without if expressions these would have to be wrapped in a function which would be both heavy and obscure. For example suppose a procedure P has two parameters P1 and P2 and that the precondition is that if P1 is positive then P2 must also be positive but if P1 is not positive then there is no restriction on P2. We could express this by writing a function such as
function Checkparas(P1, P2: Integer) return Boolean is
   if P1 > 0 then
      return P2 > 0;
   else    -- P1 is not positive
      return True;    -- so don't care about P2
   end if;
end Checkparas;
and then we can write
procedure P(P1, P2: Integer)
   with Pre => Checkparas(P1, P2);
This is truly gruesome. Apart from the effort of having to declare the wretched function Checkparas, the consequence is that the meaning of the precondition can only be determined by looking at the body of Checkparas and that could be miles away, typically in the body of the package containing the declaration of P. This would be a terrible violation of information hiding in reverse; we would be forced to hide something that should be visible.
However, using if expressions we can simply write
Pre => (if P1 > 0 then P2 > 0 else True);
and this can be abbreviated to
Pre => (if P1 > 0 then P2 > 0);
because there is a convenient rule that a trailing else True can be omitted when the type is a Boolean type. Many will find it much easier to read without else True anyway since it is similar to saying P1 > 0 implies P2 > 0. Adding an operation such as implies was considered but rejected as unnecessary.
The precondition could be extended to say that if P1 equals zero then P2 also has to be zero but if P1 is negative then we continue not to care about P2. This would be written thus
Pre => (if P1 > 0 then P2 > 0 elsif P1 = 0 then P2 = 0);
There are various sensible rules about the types of the various branches in an if expression as expected. Basically, they must all be of the same type or convertible to the same expected type. Thus consider a procedure Do_It taking a parameter of type Float and the call
Do_It (if B then X else 3.14);
where X is a variable of type Float. Clearly we wish to permit this but the two branches of the if statement are of different types, X is of type Float whereas 3.14 is of type universal_real. But a value of type universal_real can be implicitly converted to Float which is the type expected by Do_It and so all is well.
There are also rules about accessibility in the case where the various branches are of access types; the details need not concern us in this overview!
The other new form of conditional expression is the case expression and this follows similar rules to the if expression just discussed. Here is an amusing example based on one in the AI which introduces case expressions.
Suppose we are making a fruit salad and add various fruits to a bowl. We need to check that the fruit is in an appropriate state before being added to the bowl. Suppose we have just three fruits given by
type Fruit_Kind is
 (Apple, Banana, Pineapple);
then we might have a procedure Add_To_Salad thus
procedure Add_To_Salad(Fruit: in Fruit_Type);
where Fruit_Type is perhaps a discriminated type thus
type Fruit_Type (Kind: Fruit_Kind) is private;
In addition there might be functions such as Is_Peeled that interrogate the state of a fruit.
We could then have a precondition that checks that the fruit is in an edible state thus
Pre =>    (if Fruit.Kind = Apple then Is_Crisp(Fruit)
                elsif Fruit.Kind = Banana then Is_Peeled(Fruit)
                elsif Fruit.Kind = Pineapple then Is_Cored(Fruit));
(This example is all very well but it has allowed the apple to go in uncored and the pineapple still has its prickly skin.)
Now suppose we decide to add Orange to type Fruit_Kind. The precondition will still work in the sense that the implicit else True will allow the orange to pass the precondition unchecked and will go into the fruit salad possibly unpeeled, unripe or mouldy. The trouble is that we have lost the full coverage check which is such a valuable feature of case statements and aggregates in Ada.
We overcome this by using a case expression and writing
Pre => (case Fruit.Kind is
               when Apple => Is_Crisp(Fruit),
               when Banana => Is_Peeled(Fruit),
               when Pineapple => Is_Cored(Fruit),
               when Orange => Is_Peeled(Fruit));
and of course without the addition of the choice for Orange it would fail to compile.
Note that there is no end case just as there is no end if in an if expression. Moreover, like the if expression, the case expression must be in parentheses. Similar rules apply regarding the types of the various branches and so on.
Of course, the usual rules of case statements apply and so we might decide not to bother about checking the crispness of the apple but to check alongside the pineapple (another kind of apple!) that it has been cored by writing
Pre => (case Fruit.Kind is
               when Apple | Pineapple => Is_Cored(Fruit),
               when Banana | Orange => Is_Peeled(Fruit));
We can use others as the last choice as expected but this would lose the value of coverage checking. There is no default when others => True corresponding to else True for if expressions because that would defeat coverage checking completely.
A further new form of expression is the so-called quantified expression. Quantified expressions allow the checking of a Boolean expression for a given range of values and will again be found useful in pre- and postconditions. There are two forms using for all and for some. Note carefully that some is a new reserved word.
Suppose we have an integer array type
type Atype is array (Integer range <>) of Integer;
then we might have a procedure that sets each element of an array of integers equal to its index. Its specification might include a postcondition thus
procedure Set_Array(A: out Atype)
   with Post => (for all M in A'Range => A(M) = M);
This is saying that for all values of M in A'Range we want the expression A(M) = M to be true. Note how the two parts are separated by =>.
We could devise a function to check that some component of the array has a given value by
function Value_Present(A: Atype; X: Integer) return Boolean
   with Post => Value_Present'Result = (for some M in A'Range => A(M) = X);
Note the use of Value_Present'Result to denote the result returned by the function Value_Present.
As with conditional expressions, quantified expressions are always enclosed in parentheses.
The evaluation of quantified expressions is as expected. Each value of M is taken in turn (as in a for statement and indeed we could insert reverse) and the expression to the right of => then evaluated. In the case of universal quantification (a posh term meaning for all) as soon as one value is found to be False then the whole quantified expression is False and no further values are checked; if all values turn out to be True then the quantified expression is True. A similar process applies to existential quantification (that is for some) where the roles of True and False are reversed.
Those with a mathematical background will be familiar with the symbols ∀ and ∃ which correspond to for all and for some respectively. Readers are invited to discuss whether the A is upside down and the E backwards or whether they are both simply rotated.
As a somewhat more elaborate example suppose we have a function that finds the index of the first value of M such that A(M) equals a given value X. This needs a precondition to assert that such a value exists.
function Find(A: Atype; X: Integer) return Integer
      Pre => (for some M in A'Range => A(M) = X),
      Post => A(Find'Result) = X and
           (for all M in A'First .. Find'Result–1 => A(M) /= X);
Note again the use of Find'Result to denote the result returned by the function Find.
Quantified expressions can be used in any context requiring an expression and are not just for pre- and postconditions. Thus we might test whether an integer N is prime by
RN := Integer(Sqrt(Float(N)));
if (for some K in 2 .. RN => N mod K = 0) then
   ...    -- N not prime
or we might reverse the test by
if (for all K in 2 .. RN => N mod K / = 0) then
   ...    -- N is prime
Beware that this is not a recommended technique if N is at all large!
We noted above that a major reason for introducing if expressions and case expressions was to avoid the need to introduce lots of auxiliary functions for contexts such as preconditions. Nevertheless the need still arises from time to time. A feature of existing functions is that the code is in the body and this is not visible in the region of the precondition – information hiding is usually a good thing but here it is a problem. What we need is a localized and visible shorthand for a little function. After much debate, Ada 2012 introduces expression functions which are essentially functions whose visible body comprises a single expression. Thus suppose we have a record type such as
type Point is tagged
      X, Y: Float := 0.0;
   end record;
and the precondition we want for several subprograms is that a point is not at the origin. Then we could write
function Is_At_Origin(P: Point) return Boolean is
   (P.X = 0.0 and P.Y = 0.0);
and then
procedure Whatever(P: Point; ...)
   with Pre => not P.Is_At_Origin;
and so on.
Such a function is known as an expression function; naturally it does not have a distinct body. The expression could be any expression and could include calls of other functions (and not just expression functions). The parameters could be of any mode (see next section).
Expression functions can also be used as a completion. This arises typically if the type is private. In that case we cannot access the components P.X and P.Y in the visible part. However, we don't want to have to put the code in the package body. So we declare a function specification in the visible part in the normal way thus
function Is_At_Origin(P: Point) return Boolean;
and then an expression function in the private part thus
   type Point is ...
   function Is_At_Origin(P: Point) return Boolean is
      (P.X = 0.0 and P.Y = 0.0);
and the expression function then completes the declaration of Is_At_Origin and no function body is required in the package body.
Observe that we could also use an expression function for a completion in a package body so that rather than writing the body as
function Is_At_Origin(P: Point) return Boolean is
   return P.X = 0.0 and P.Y = 0.0;
end Is_At_Origin;
we could write an expression function as a sort of shorthand.
Incidentally, in Ada 2012, we can abbreviate a null procedure body in a similar way by writing
procedure Nothing(...) is null;
as a shorthand for
procedure Nothing(...) is
end Nothing;
and this will complete the procedure specification
procedure Nothing(...);
Another change in this area is that membership tests are now generalized. In previous versions of Ada, membership tests allowed one to see whether a value was in a range or in a subtype, thus we could write either of 
if D in 1 .. 30 then
if D in Days_In_Month then
but we could not write something like
if D in 1 | 3 | 5 | 6 ..10 then
This is now rectified and following in we can now have one or more of a value, a range, or a subtype or any combination separated by vertical bars. Moreover, they do not have to be static.
A final minor change is that the form qualified expression is now treated as a name rather than as a primary. Remember that a function call is treated as a name and this allows a function call to be used as a prefix.
For example suppose F returns an array (or more likely an access to an array) then we can write
and this returns the value of the component with index N. However, suppose the function is overloaded so that this is ambiguous. The normal technique to overcome ambiguity is to use a qualified expression and write T'(F(...)). But in Ada 2005 this is not a name and so cannot be used as a prefix. This means that we typically have to copy the array (or access) and then do the indexing or (really ugly) introduce a dummy type conversion and write T(T'(F(...)))(N). Either way, this is a nuisance and hence the change in Ada 2012.

Contents   Index   References   Search   Previous   Next 
© 2011, 2012, 2013 John Barnes Informatics.
Sponsored in part by:
The Ada Resource Association:


and   Ada-Europe: