[Ada Information Clearinghouse]

Ada '83 Quality and Style:

Guidelines for Professional Programmers

Copyright 1989, 1991,1992 Software Productivity Consortium, Inc., Herndon, Virginia.

CHAPTER 5: Programming Practices

5.6 Statements

Careless or convoluted use of statements can make a program hard to read and maintain even if its global structure is well organized. You should strive for simple and consistent use of statements to achieve clarity of local program structure. Some of the guidelines in this section counsel use or avoidance of particular statements. As pointed out in the individual guidelines, rigid adherence to those guidelines would be excessive, but experience has shown that they generally lead to code with improved reliability and maintainability.
In this section...
5.6.1 Nesting
5.6.2 Slices
5.6.3 Case Statements
5.6.4 Loops
5.6.5 Exit Statements
5.6.6 Recursion and Iteration Bounds
5.6.7 Goto Statements
5.6.8 Return Statements
5.6.9 Blocks
5.6.10 Aggregates
Summary of Guidelines from this section

Language Ref Manual references: 5 Statements


5.6.1 Nesting

guideline

instantiation

example

The following section of code:
if not Condition_1 then

   if Condition_2 then 
      Action_A; 
   else  -- not Condition_2 
      Action_B; 
   end if;
   
else  -- Condition_1 
   Action_C; 
end if;

can be rewritten more clearly and with less nesting as:
if Condition_1 then 
   Action_C; 
elsif Condition_2 then 
   Action_A;
   
else  -- not (Condition_1 or Condition_2) 
   Action_B; 
end if;

rationale

Deeply nested structures are confusing, difficult to understand, and difficult to maintain. The problem lies in the difficulty of determining what part of a program is contained at any given level. For expressions, this is important in achieving the correct placement of balanced grouping symbols and in achieving the desired operator precedence. For control structures, the question involves what part is controlled. Specifically, is a given statement at the proper level of nesting, i.e., is it too deeply or too shallowly nested, or is the given statement associated with the proper choice, e.g., for if or case statements? Indentation helps, but it is not a panacea. Visually inspecting alignment of indented code (mainly intermediate levels) is an uncertain job at best. To minimize the complexity of the code, keep the maximum number of nesting levels between three and five.

note

Ask yourself the following questions to help you simplify the code:

exceptions

If deep nesting is required frequently, there may be overall design decisions for the code that should be changed. Some algorithms require deeply nested loops and segments controlled by conditional branches. Their continued use can be ascribed to their efficiency, familiarity, and time proven utility. When nesting is required, proceed cautiously and take special care with the choice of identifiers and loop and block names.

Language Ref Manual references: 5.1 Simple and Compound Statements - Sequences of Statements, 5.3 If Statements, 5.4 Case Statements, 5.5 Loop Statements, 5.6 Block Statements, 9.5 Entries, Entry Calls, and Accept Statements, 9.7 Select Statements


5.6.2 Slices

guideline

example

First  : constant Index := Index'First; 
Second : constant Index := Index'Succ(First); 
Third  : constant Index := Index'Succ(Second);

type Vector is array (Index range <>) of Element;

subtype Column_Vector is Vector (Index); 
type    Square_Matrix is array  (Index) of Column_Vector;

subtype Small_Range  is Index range First .. Third; 
subtype Diagonals    is Vector (Small_Range); 
type    Tri_Diagonal is array  (Index) of Diagonals;

Markov_Probabilities : Square_Matrix; 
Diagonal_Data        : Tri_Diagonal;

...

-- Remove diagonal and off diagonal elements. 
Diagonal_Data(Index'First)(First) := Null_Value; 
Diagonal_Data(Index'First)(Second .. Third) := 
      Markov_Probabilities(Index'First)(First .. Second);
      
for I in Second .. Index'Pred(Index'Last) loop 
   Diagonal_Data(I) := 
         Markov_Probabilities(I)(Index'Pred(I) .. Index'Succ(I)); 
end loop;

Diagonal_Data(Index'Last)(First .. Second) := 
      Markov_Probabilities(Index'Last) 
         (Index'Pred(Index'Last) .. Index'Last); 
Diagonal_Data(Index'Last)(Third) := Null_Value;

rationale

An assignment statement with slices is simpler and clearer than a loop, and helps the reader see the intended action. Slice assignment can be faster than a loop if a block move instruction is available.

Language Ref Manual references: 3.6.2 Operations of Array Types, 4.1.2 Slices, 5.2 Assignment Statement, 5.5 Loop Statements


5.6.3 Case Statements

guideline

example

type Color is (Red, Green, Blue, Purple); 
Car_Color : Color := Red;

...

case Car_Color is 
   when Red .. Blue => ... 
   when Purple      => ... 
end case;  -- Car_Color

Now consider a change in the type:

type Color is (Red, Yellow, Green, Blue, Purple);

This change may have an unnoticed and undesired effect in the case statement. If the choices had been enumerated explicitly, as when Red | Green | Blue => instead of when Red .. Blue =>, then the case statement would have not have compiled. This would have forced the maintainer to make a conscious decision about what to do in the case of Yellow.

rationale

All possible values for an object should be known and should be assigned specific actions. Use of an others clause may prevent the developer from carefully considering the actions for each value. A compiler warns the user about omitted values, if an others clause is not used.

Each possible value should be explicitly enumerated. Ranges can be dangerous because of the possibility that the range could change and the case statement may not be reexamined.

exception

It is acceptable to use ranges for possible values only when the user is certain that new values will never be inserted among the old ones, as for example, in the range of ASCII characters: 'a' .. 'z'.

note

Ranges that are needed in case statements can use constrained subtypes to enhance maintainability. It is easier to maintain because the declaration of the range can be placed where it is logically part of the abstraction, not buried in a case statement in the executable code.
subtype Lower_Case is Character range       'a' ..      'z'; 
subtype Upper_Case is Character range       'A' ..      'Z'; 
subtype Control    is Character range ASCII.Nul .. ASCII.Us; 
subtype Numbers    is Character range       '0' ..      '9';

... 
case Input_Char is 
   when Lower_Case =>         Capitalize(Input_Char); 
   when Upper_Case =>         null; 
   when Control    =>         raise Invalid_Input; 
   when Numbers    =>         null; 
   ... 
end case;
Language Ref Manual references: 3.3.2 Subtype Declarations, 3.5.1 Enumeration Types, 5.4 Case Statements


5.6.4 Loops

guideline

example

To iterate over all elements of an array:
for 1 in Array_Name'Range loop 
   ... 
end loop;
To iterate over all elements in a linked list:
Pointer := Head_Of_List; 
while Pointer /= null loop 
   ... 
   Pointer := Pointer.Next; 
end loop;

Situations requiring a "loop and a half" arise often. For this use:
P_And_Q_Processing: 
   loop 
      P; 
      exit P_And_Q_Processing when Condition_Dependent_On_P; 
      Q; 
   end loop P_And_Q_Processing;

rather than:
P; 
while not Condition_Dependent_On_P loop 
   Q; 
   P; 
end loop;

rationale

A for loop is bounded, so it cannot be an "infinite loop." This is enforced by the Ada language which requires a finite range in the loop specification and which does not allow the loop counter of a for loop to be modified by a statement executed within the loop. This yields a certainty of understanding for the reader and the writer not associated with other forms of loops. A for loop is also easier to maintain because the iteration range can be expressed using attributes of the data structures upon which the loop operates, as shown in the example above where the range changes automatically whenever the declaration of the array is modified. For these reasons, it is best to use the for loop whenever possible; that is, whenever simple expressions can be used to describe the first and last values of the loop counter.

The while loop has become a very familiar construct to most programmers. At a glance it indicates the condition under which the loop continues. Use the while loop whenever it is not possible to use the for loop, but there is a simple boolean expression describing the conditions under which the loop should continue, as shown in the example above.

The plain loop statement should be used in more complex situations, even if it is possible to contrive a solution using a for or while loop in conjunction with extra flag variables or exit statements. The criteria in selecting a loop construct is to be as clear and maintainable as possible. It is a bad idea to use an exit statement from within a for or while loop because it is misleading to the reader after having apparently described the complete set of loop conditions at the top of the loop. A reader who encounters a plain loop statement expects to see exit statements.

There are some familiar looping situations which are best achieved with the plain loop statement. For example, the semantics of the Pascal repeat until loop, where the loop is always executed at least once before the termination test occurs, are best achieved by a plain loop with a single exit at the end of the loop. Another common situation is the "loop and a half" construct, shown in the example above, where a loop must terminate somewhere within the sequence of statements of the body. Complicated "loop and a half" constructs simulated with while loops often require the introduction of flag variables, or duplication of code before and during the loop, as shown in the example. Such contortions make the code more complex and less reliable.

Minimize the number of ways to exit a loop in order to make the loop more understandable to the reader. It should be rare that you need more than two exit paths from a loop. When you do, be sure to use exit statements for all of them, rather than adding an exit statement to a for or while loop.

Language Ref Manual references: 5.5 Loop Statements, 5.7 Exit Statements


5.6.5 Exit Statements

guideline

example

See the examples in Guidelines 5.1.1 and 5.6.4.

rationale

It is more readable to use exit statements than to try to add boolean flags to a while loop condition to simulate exits from the middle of a loop. Even if all exit statements would be clustered at the top of the loop body, the separation of a complex condition into multiple exit statements can simplify and make it more readable and clear. The sequential execution of two exit statements is often more clear than the short-circuit control forms.

The exit when form is preferable to the if ... then, exit form because it makes the word exit more visible by not nesting it inside of any control construct. The if ... then exit form is needed only in the case where other statements, in addition to the exit statement, must be executed conditionally. For example:
if Status = Done then

   Shut_Down; 
   return;
   
end if;
Loops with many scattered exit statements can indicate fuzzy thinking as regards the loop's purpose in the algorithm. Such an algorithm might be coded better some other way, e.g., with a series of loops. Some rework can often reduce the number of exit statements and make the code clearer.

See also Guidelines 5.1.3 and 5.6.4.

Language Ref Manual references: 5.3 If Statements, 5.7 Exit Statements


5.6.6 Recursion and Iteration Bounds

guideline

example

Establishing an iteration bound:
Safety_Counter := 0;

Process_List: 
   loop 
      exit when Current_Item = null;
      
      ... 
      Current_Item := Current_Item.Next;
      
      ... 
      Safety_Counter := Safety_Counter + 1; 
      if Safety_Counter > 1_000_000 then 
         raise Safety_Error; 
      end if;
      
   end loop Process_List;
Establishing a recursion bound:
procedure Depth_First (Root           : in     Tree; 
                       Safety_Counter : in     Recursion_Bound 
                                      := Recursion_Bound'Last) is 
begin 
   if Root /= null then
   
      if Safety_Counter = 0 then 
         raise Recursion_Error; 
      end if;
      
      Depth_First 
           (Root.Left_Branch,   Safety_Counter - 1); -- recursive call 
      Depth_First 
           (Root.Right_Branch,  Safety_Counter - 1); -- recursive call
           
      ... -- normal subprogram body 
   end if;
   
end Depth_First;
Following are examples of this subprogram's usage. One call specifies a maximum recursion depth of 50. The second takes the default (one thousand). The third uses a computed bound:
Depth_First(Root, 50); 
Depth_First(Root); 
Depth_First(Root, Current_Tree_Height);

rationale

Recursion, and iteration using structures other than for statements, can be infinite because the expected terminating condition does not arise. Such faults are sometimes quite subtle, may occur rarely, and may be difficult to detect because an external manifestation might be absent or substantially delayed.

By including counters and checks on the counter values, in addition to the loops themselves, you can prevent many forms of infinite loops. The inclusion of such checks is one aspect of the technique called Safe Programming (Anderson and Witty 1978).

The bounds of these checks do not have to be exact, just realistic. Such counters and checks are not part of the primary control structure of the program but a benign addition functioning as an execution-time "safety net" allowing error detection and possibly recovery from potential infinite loops or infinite recursion.

note

If a loop uses the for iteration scheme (Guideline 5.6.4), it follows this guideline.

exceptions

Embedded control applications have loops that are intended to be infinite. Only a few loops within such applications should qualify as exceptions to this guideline. The exceptions should be deliberate (and documented) policy decisions.

This guideline is most important to safety critical systems. For other systems, it may be overkill.

Language Ref Manual references: 5.5 Loop Statements, 6.1 Subprogram Declarations, 6.4 Subprogram Calls


5.6.7 Goto Statements

guideline

rationale

A goto statement is an unstructured change in the control flow. Worse, the label does not require an indicator of where the corresponding goto statement(s) are. This makes code unreadable and makes its correct execution suspect.

Other languages use goto statements to implement loop exits and exception handling. Ada's support of these constructs makes the goto statement extremely rare.

note

If you should ever use a goto statement, highlight both it and the label with blank space. Indicate at the label where the corresponding goto statement(s) may be found.

Language Ref Manual references: 2.7 Comments, 5.9 Goto Statements


5.6.8 Return Statements

guideline

example

The following code fragment is longer and more complex than necessary:
if Pointer /= null then

   if Pointer.Count > 0 then 
      return True;
      
   else  -- Pointer.Count = 0 
      return False; 
   end if;
   
else  -- Pointer = null 
   return False; 
end if;
It should be replaced with the shorter, more concise, and clearer equivalent line:

return Pointer /= null and then Pointer.Count > 0;

rationale

Excessive use of returns can make code confusing and unreadable. Only use return statements where warranted. Too many returns from a subprogram may be an indicator of cluttered logic. If the application requires multiple returns, use them at the same level (i.e., as in different branches of a case statement), rather than scattered throughout the subprogram code. Some rework can often reduce the number of return statements to one and make the code more clear.

exception

Do not avoid return statements if it detracts from natural structure and code readability.

Language Ref Manual references: 2.7 Comments, 5.8 Return Statements


5.6.9 Blocks

guideline

example

with Motion; 
with Accelerometer_Device; 
...

   --------------------------------------------------------------------- 
   function Maximum_Velocity return Motion.Velocity is
   
      Cumulative : Motion.Velocity := 0.0;
      
   begin  -- Maximum_Velocity
   
      -- Initialize the needed devices 
      ...
      
      Calculate_Velocity_From_Sample_Data: 
         declare 
            Current       : Motion.Acceleration := 0.0; 
            Accelerometer : Accelerometer_Device.Interface; 
            Time_Delta    : Duration;
            
         begin  -- Calculate_Velocity_From_Sample_Data 
            for I in 1 .. Accelerometer_Device.Sample_Limit loop
            
               Get_Samples_And_Ignore_Invalid_Data: 
                  begin 
                     Accelerometer.Value(Current, Time_Delta); 
                  exception 
                     when Numeric_Error | Constraint_Error => 
                        null; -- Continue trying
                        
                     when Accelerometer_Device.Failure => 
                        raise Accelerometer_Device_Failed; 
                  end Get_Samples_And_Ignore_Invalid_Data;
                  
               exit when Motion."<"(Current, 0.0); -- Slowing down
               
               Update_Velocity: 
                  declare 
                     use Motion; -- for infix operators and exceptions;
                     
                  begin 
                     Cumulative := Cumulative + Current * Time_Delta;
                     
                  exception 
                     when Numeric_Error | Constraint_Error => 
                        raise Maximum_Velocity_Exceeded; 
                  end Update_Velocity;
                  
            end loop; 
         end Calculate_Velocity_From_Sample_Data;
         
      return Cumulative;
      
   end Maximum_Velocity; 
   --------------------------------------------------------------------- 
...

rationale

Blocks break up large segments of code and isolate details relevant to each subsection of code. Variables that are only used in a particular section of code are clearly visible when a declarative block delineates that code.

Renaming may simplify the expression of algorithms and enhance readability for a given section of code. But it is confusing when a rename clause is visually separated from the code to which it applies. The declarative region allows the renames to be immediately visible when the reader is examining code which uses that abbreviation. Guideline 5.7.1 discusses a similar guideline concerning the `use' clause.

Local exception handlers can catch exceptions close to the point of origin and allow them to either be handled, propagated, or converted.

Language Ref Manual references: 5.6 Block Statements, 8.4 Use Clauses, 8.5 Renaming Declarations, 11.2 Exception Handlers


5.6.10 Aggregates

guideline

example

It is better to use aggregates:
Set_Position((X, Y));

Employee_Record :=  (Number     => 42, 
                     Age        => 51, 
                     Department => Software_Engineering);
than to use consecutive assignments or temporary variables:
Temporary_Position.X := 100; 
Temporary_Position.Y := 200; 
Set_Position(Temporary_Position);

Employee_Record.Number     := 42; 
Employee_Record.Age        := 51; 
Employee_Record.Department := Software_Engineering;

rationale

Using aggregates during maintenance is beneficial. If a record structure is altered, but the corresponding aggregate is not, the compiler flags the missing field in the aggregate assignment. It would not be able to detect the fact that a new assignment statement should have been added to a list of assignment statements.

Aggregates can also be a real convenience in combining data items into a record or array structure required for passing the information as a parameter. Named component association makes aggregates more readable.

Language Ref Manual references: 4.3.1 Record Aggregates


Back to document index