Rationale for Ada 2012
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
begin
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
with
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
record
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
private
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
begin
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
begin
null;
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
F(...)(N)
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.
© 2011, 2012, 2013 John Barnes Informatics.
Sponsored in part by: