John English wrote a wonderful book titled Ada 95: The
Craft of Object-Oriented Programming, which is available on line at http://archive.adaic.com/docs/craft/html/contents.htm. In chapter 3 of that book he explores many
Ada programming fundamentals using a simple calculator program example. The
code for that example is:
with
Ada.Text_IO, Ada.Integer_Text_IO;
use Ada.Text_IO,
Ada.Integer_Text_IO;
procedure Calculator is
Result : Integer;
Operator : Character;
Operand : Integer;
begin
Put ("Enter an expression: ");
Get (Result); -- 1
loop
-- 2
loop
-- 3
Get (Operator);
exit when Operator /= ' ';
end loop;
if Operator = '.' then -- 4
Put (Result, Width => 1); -- 5
exit;
-- 6
else
Get (Operand);
-- 7
case Operator is
when '+' =>
Result := Result +
Operand; -- 8
when '-' =>
Result := Result -
Operand;
when '*' =>
Result := Result *
Operand; -- 9
when '/' =>
Result := Result /
Operand;
when others =>
Put ("Invalid
operator '"); -- 10
Put (Operator);
Put ("'");
exit; -- 11
end case;
end if;
end loop;
New_Line;
end
Calculator;
|
This program implements a simple left-to-right calculator that does not
understand parentheses. Each calculation string is terminated with a period
(full stop for those speaking the King’s English).
1 + 2 * 3.
The calculation string above is evaluated left to right, yielding the
value 9. The calculator does not impose any operator precedence.
The internal loop beginning with the line labeled 3 in an end-of-line
comment deals with the fact that the Get procedure for type Character simply
gets the next character in the input stream. It does not skip space characters
to input the next non-space character. The loop implemented by John English
simply reads characters until it encounters something that is not a space
character. The hope implicit in this program is that the person creating the
input strings only uses integral numbers, spaces, or one of the characters ‘+’,
‘-‘, ‘*’, ‘-‘, or ‘.’. If it encounters any other character it declares that an
invalid operator has been input and terminates the program.
As a software safety engineer I am always uncomfortable with a program
that cannot clearly and concisely specify simple input values. What happens,
for instance, in the John English version of this program if the person running
the program inputs horizontal tabs instead of spaces? They may look similar on
an editor screen, but they are distinctly different to program.
I know that John English was well aware of these issues, but was trying
to introduce students to the fundamentals of Ada programming rather than the
complications of error handling. In fact, until the 2012 version of Ada there
was no simple way to do better.
Ada 2012 introduced aspect clauses. The form of the aspect clause of
interest to this article is the static predicate. A static predicate is a
qualification of a type or subtype definition defining a set of properties
about the type. In Ada 95 the best you could do was to specify the range of
values for a subtype or type. That range had to be contiguous, with no gaps in
the values. The Ada 2012 static predicate definition introduces more
flexibility. It allows any expression that can be evaluated as a membership
test.
How does this relate to the calculator program shown above? The
operators in that program are five distinct characters, but not a contiguous
range of characters. Let’s see how we could express that set of characters
using an Ada 2012 static predicate.
Subtype Acceptable is
Character
with Static_Predicate => Acceptable in
‘+’|’-‘|’*’|’/’|’.’;
The result is a subtype of Character with five valid values.
Once we have established a subtype with only the few values
we want to use, we have managed to more clearly express the intent of our
program. A comparable program to the Calculator program above, but using this
static predicate expression is:
------------------------------------------------------------------
-- Simple Calculator
Program
--
------------------------------------------------------------------
with Ada.Text_IO; use
Ada.Text_IO;
with Ada.Integer_Text_Io; use
Ada.Integer_Text_IO;
procedure Simple_Calculator
is
subtype Acceptable is Character
with Static_Predicate => Acceptable in
'+'|'-'|'*'|'/'|'.';
function Get_Operator return Acceptable is
Temp : Character;
begin
loop
get(Temp);
exit when Temp in Acceptable;
end loop;
return Temp;
end Get_Operator;
Operator : Acceptable;
Operand
: Integer;
Result
: Integer;
begin
Get(Item => Result);
Outer:
loop
Operator := Get_Operator;
if Operator = '.' then
Put(Item => Result, Width =>
1);
New_Line;
exit Outer when End_Of_File;
Get(Item => Result);
else
Get(Item => Operand);
case Operator is
when '+' => Result := Result +
Operand;
when '-' => Result := Result -
Operand;
when '*' => Result := Result *
Operand;
when '/' => Result := Result /
Operand;
when '.' => null;
end case;
end if;
end loop Outer;
end Simple_Calculator;
|
I have encapsulated the operator input loop into a function
to clearly specify my intentions. Note that the function Get_Operator now loops
until it reads a character that is a member of the subtype Acceptable. This
logic will read past spaces, tabs, or any other characters that are not members
of the subtype Acceptable. The case statement now does not need a “when Others”
clause because it explicitly handles all possible values of subtype Acceptable.
If another operator were to be added to the set of values for Acceptable the
compiler would flag the need to handle the new value in the case statement.
The use of the Static Predicate to create a discontinuous
set of valid values can be very useful. It also greatly simplifies programming
over creating a more complex set of conditional statements on an input loop.
Finally, it allows a more robust use of the case clause.
No comments:
Post a Comment