Examples Overview > Ada Programming Guidelines

Ada Programming Guidelines

Copyright © 1997 Rational Software Corporation.
All rights reserved.

The word "Rational" and Rational's products are trademarks of Rational Software Corporation. References to other companies and their products use trademarks owned by the respective companies and are for reference purpose only.

Contents

About this Document

Introduction

Fundamental Principles
Assumptions
Classification of Guidelines
The First and Last Guideline

Code Layout

General
Letter Case
Indentation
Line Length and Line Breaks
Alignments

Comments

General
Guidelines for the Use of Comments

Naming Conventions

General
Packages
Types
Exceptions
Subprograms
Objects and Subprogram (or Entry) Parameters
Generic Units
Naming Strategies for Subsystems

Declarations of Types, Objects, and Program Units

Enumeration Types
Numeric Types
Real Types
Record Types
Access Types
Private Types
Derived Types
Object Declarations
Subprograms and Generic Units

Expressions and Statements

Expressions
Statements
Coding Hints

Visibility Issues

Overloading and Homographs
Context Clauses
Renamings
Note about Use Clauses

Program Structure and Compilation Issues

Decomposition of Packages
Structure of Declarative Parts
Context Clauses
Elaboration Order

Concurrency

Error Handling and Exceptions

Low-Level Programming

Representation Clauses and Attributes
Unchecked Conversions

Summary

References

Glossary


Chapter 1

About this Document

This document Rational Unified Process - Ada Programming Guidelines is a template that can be used to derive a coding standard for your own organization. Its specifies how Ada programs must be written. Its intended audience are all application software designers and developers who use Ada as the implementation language, or as a design language for specifying interfaces or data structures for example.

The rules described in this document cover most aspects of coding. General rules apply to program layout, naming conventions, use of comments. Specific rules apply to selected Ada features and specify forbidden constructs, recommended usage patterns, and general hints to enhance program quality.

There is a certain degree of overlap between the project design guidelines and the present programming guidelines, and this is intentional. Many coding rules, especially in the area of naming conventions, have been introduced to actively support and reinforce an object-oriented approach to software design.

The guidelines were originally written for Ada 83. They include compatibility rules with Ada 95, but no specific guidelines for the use of the new features of the language introduced in the revised language standard, such as tagged types, child units or decimal types.

The document organization follows loosely the structure of the Ada Reference Manual [ISO 8052].

Chapter 2, Introduction, explains the fundamental principles on which the guidelines are based, and introduces a classification of guidelines.

Chapter 3, Code layout, deals with the general visual organization of the text of the programs.

Chapter 4, Comments, gives guidance on how to use comments to document the code in a structured, useful and maintainable fashion.

Chapter 5, Naming conventions, gives some general rules about naming language entities, and examples. This chapter must be tailored to suit the needs of your particular project or organization.

Chapter 6, Declarations, and Chapter 7, Expression and statements, give further advice on each kind of language construct.

Chapter 8, Visibility issues, and Chapter 9, Program structure and compilation issues, give guidance on global structuring and organization of the programs.

Chapter 10, Concurrency, deals with the specialized topic of using tasking and time-related features of the language.

Chapter 11, Error-handling and exceptions gives some guidance on how to use or not use exception to handle errors in a systematic and light-weight fashion.

Chapter 12, Low-level programming, deals with issues of representation clauses.

Chapter 13, Summary, recapitulates the most important guidelines.

This document replaces Ada Guidelines: Recommendations for Designers and programmers, Application Note #15, Rational, Santa Clara, CA., 1990.


Chapter 2

Introduction

Fundamental Principles

Ada was explicitly designed to support the development of high-quality, reliable, reusable, and portable software [ISO 87, sect. 1.3]. However, no programming language on its own can ensure that this is achieved. Programming has to be done as part of a well-disciplined process.

Clear, understandable Ada source code is the primary goal of most of the guidelines provided here. This is a major contributing factor to reliability and maintainability. What is meant by clear and understandable code can be captured in the following three simple fundamental principles.

Minimal Surprise

Over its lifetime, source code is read more often than it is written, especially specifications. Ideally, code should read like an English-language description of what is being done, with the added benefit that it executes. Programs are written more for people than for computers. Reading code is a complex mental process that can be supported by uniformity, also referred to in this guide as the minimal-surprise principle. A uniform style across an entire project is a major reason for a team of software developers to agree on programming standards, and it should not be perceived as some kind of punishment or as an obstacle to creativity and productivity.

Single Point of Maintenance

Another important principle underlying this guide is the single-point-of-maintenance principle. Whenever possible, a design decision should be expressed at only one point in the Ada source, and most of its consequences should be derived programmatically from this point. Violations of this principle greatly jeopardize maintainability and reliability, as well as understandability.

Minimal Noise

Finally, as a major contribution to legibility, the minimal-noise principle has been applied. That is, an effort has been made to avoid cluttering the source code with visual "noise": bars, boxes, and other text with low information content or information that does not contribute to the understanding of the purpose of the software.

Portability and reusability are also reasons for many of the guidelines. The code will have to be ported to several different compilers for different target computers, and eventually to a more advanced version of Ada, called "Ada 95" [PLO92, TAY92].

Assumptions

The guidelines presented here make a small number of basic assumptions:

The reader knows Ada.

The use of advanced Ada features is encouraged wherever beneficial, rather than discouraged on the ground that some programmers are unfamiliar with them. This is the only way in which the project can really benefit from using Ada. Ada should not be used as if it were Pascal or FORTRAN. Paraphrasing the code in comments is discouraged; on the contrary, Ada should be used in place of comments wherever feasible.

The reader knows English.

Many of the naming conventions are based on English, both vocabulary and syntax. Moreover, Ada keywords are common English words, and mixing them with another language degrades legibility.

The use of use clauses is highly restricted.

Naming conventions and a few other rules assume that "use" clauses are not used.

A very large project is being dealt with.

Many rules offer the most value in large Ada systems, although they can also be used in a small system, if only for the sake of practice and uniformity at the project or corporate level.

Source code is being developed on the Rational Environment.

By using the Rational Environment, issues such as code layout, identifiers in closing constructs, and so on are taken care of by the Ada editor and formatter. However, the layout recommendations contained in this document can be applied on any development platform.

Coding follows an object-oriented design

Many rules will support a systematic mapping of object-oriented (OO) concepts to Ada features and specific naming conventions.

Classification of Guidelines

These guidelines are not of equal importance. They roughly follow this scale:

Hint:

The guideline is a simple piece of advice; there is no real harm done by not following it, and it can be selected or rejected as a matter of taste. Hints are marked in this document with the above symbol.

Recommendation:

The guideline is usually based on more technical grounds; portability or reusability may be affected, as well as performance in some implementations. Recommendations must be followed unless there is a good reason not to. Some exceptions are mentioned in this document. Recommendations are marked in this document by the above symbol.

Restriction:

The feature in question is dangerous to use, but it is not completely banned; the decision to use it should be a project-level decision, and that decision should be made highly visible. Restrictions are marked in this document by the symbol presented above.

Requirement:

A violation would definitely lead to bad, unreliable, or non-portable code. Requirements cannot be violated. Requirements are marked in this document the pointing hand above.

The Rational Design Facility will be used to flag the use of restricted features and to enforce required rules and many of the recommendations.

Contrary to many other Ada coding standards, very few Ada features are in fact completely banned in these guidelines. The key to good software resides in:

  • Knowing each feature, its limitations, and its potential dangers
  • Knowing exactly in which circumstances the feature is safe to use
  • Making the decision to use the feature highly visible
  • Using the feature with great care and moderation, where appropriate.

The First and Last Guideline

Use common sense.

When you cannot find a rule or guideline, when the rule obviously does not apply, when everything else fails: use common sense, and check the fundamental principles. This rule overrides all of the others. Common sense is required.


Chapter 3

Code Layout

General

The layout of a program unit is completely under the control of the Rational Environment Formatter, and the programmer should not have to worry too much about the layout of a program, except in comments and blank space. The formatting conventions adopted by this tool are those expressed in Appendix E of the Reference Manual for the Ada Programming Language [ISO87]. In particular, they suggest that the keywords starting and ending a structured construct be vertically aligned. Also the identifier of a construct is systematically repeated at the end of the construct.

The precise behavior of the formatter is controlled by a series of library switches which receive a uniform set of values throughout the project, based on a common model world. The relevant switches are listed below with their current value for the model world we recommend.

Letter Case

Format . Id_Case : Letter_Case := Capitalized

Specifies the case of identifiers in Ada units: the very first letter, and each first letter after an underscore are in uppercase. The capitalized form is recognized as the most legible form by human readers, with most modern screen and laser printer fonts.

Format . Keyword_Case : Letter_Case := Lower

Specifies the case of Ada keywords. This distinguishes them slightly from identifiers.

Format . Number_Case : Letter_Case := Upper

Specifies the case of the letter "E" in floating-point literals and based digits ("A" to "F") in based literals.

Indentation

An Ada unit is formatted according to the general conventions expressed in Appendix E of the Ada Reference Manual [ISO87]. This means that the keywords starting and ending a structured construct are aligned. For example, "loop" and "end loop", "record" and "end record". Elements that are inside structured constructs are indented to the right.

Format . Major_Indentation : Indent_Range := 3

Specifies the number of columns that the formatter indents structured (major) constructs such as "if" statements, "case" statements, and "loop" statements.

Format . Minor_Indentation : Indent_Range := 2

Specifies the number of columns that the formatter indents minor constructs: record declarations, variant record declarations, type declarations, exception handlers, alternatives, case statements, and named and labeled statements.

Line Length and Line Breaks

Format . Line_Length : Line_Range := 80

Specifies the number of columns used by the formatter for printing lines in Ada units before wrapping them. This allows the display of formatted units with traditional VT100 like terminals.

Format . Statement_Indentation : Indent_Range := 3

Specifies the number of columns the formatter indents the second and subsequent lines of a statement when the statement has to be broken because it is longer than Line_Length. The formatter indents Statement_Indentation number of columns only if there is no lexical construct with which the indented code can be aligned.

Format . Statement_Length : Line_Range := 35

Specifies the number of columns reserved on each line to display a statement. If the current level of indentation allows for fewer than Statement_Length columns on a line, then the formatter starts over with the Wrap_Indentation column as its new level of indentation. This practice prevents deeply nested statements from being printed beyond the right margin.

Format . Wrap_Indentation : Line_Range := 16

Specifies the column at which the formatter begins the next level of indentation when the current level of indentation does not allow for Statement_Length. This practice prevents deeply nested statements from being printed beyond the right margin.

Alignments

Format . Consistent_Breaking : Integer := 1

Controls the formatting of lists of the form (xxx:aaa; yyy:bbb), which appear in subprogram formal parts and as discriminants in type declarations. It also controls formatting of lists of the form (xxx=>aaa, yyy=>bbb), which appear in subprogram calls and aggregates. Since this option is non-zero (True), when a list does not fit on a line, every element of the list begins on a new line.

Format . Alignment_Threshold : Line_Range := 20

Specifies the number of blank spaces that the formatter can insert to align lexical constructs in consecutive statements, such as colons, assignments, and arrows in named notation. If more than this number of spaces would be needed to align a construct, the construct is left unaligned.

Note that in order to force a certain layout, the programmer can insert an end-of-line, or line break that will not be removed by the formatter by entering <space> <space> <carriage-return>.

Using this technique, and in order to improve legibility and maintainability, lists of Ada elements should be broken to contain only one element per line, when the list exceeds 3 items, and when they do not fit on one line. In particular this applies to the following Ada constructs (as defined in Appendix E of the Ada Reference Manual [ISO87]):

argument association

pragma Suppress (Range_Check,
                 On => This_Type,
                 On => That_Type,                 On => That_Other_Type);      

identifier list, component list

Next_Position,
Previous_Position,
Current_Position : Position;
type Some_Record is 
    record
        A_Component,
        B_Component,
        C_Component : Component_Type;
    end record;      

enumeration type definition

type Navaid is 
       (Vor, 
        Vor_Dme, 
        Dme, 
        Tacan, 
        Vor_Tac, 
        NDB);      

discriminant constraint

subtype Constrained is Element 
        (Name_Length    => Name'Length,
         Valid          => True,
         Operation      => Skip);      

sequence of statements (done by formatter)

formal part, generic formal part, actual parameter part, generic actual parameter part

procedure Just_Do_It (This     : in Some_Type;
                      For_That : in Some Other_Type;
                      Status   : out Status_Type);
Just_Do_It (This     => This_Value;
            For_That => That_Value;
            Status   => The_Status);      

Chapter 4

Comments

General

Contrary to a widely held belief, good programs are not characterized by the number of comments, but by their quality.

Comments should be used to complement Ada code, never to paraphrase it. Ada by itself is a very legible programming language-even more so when supported by good naming conventions. Comments should supplement Ada code by explaining what is not obvious; they should not duplicate the Ada syntax or semantics. Comments should help the reader to grasp the background concepts, the dependencies, and especially complex data encoding or algorithms. Comments should highlight deviations from coding or design standards, use of restricted features, and special "tricks." Comment frames, or forms, that appear systematically for each major Ada construct (such as subprograms and packages) have the benefit of uniformity and of reminding the programmer to document the code, but they often lead to a paraphrasing style. For each comment, the programmer should be able to answer well the question: "What value is added by this comment?"

A misleading or wrong comment is worse than no comment at all. Comments (unless they participate in some formal Ada Design Language (ADL) or Program Design Language (PDL), as with the Rational Design Facility) are not checked by the compiler. Therefore, in accordance with the single-point-of-maintenance principle, design decisions should be expressed in Ada rather than in comments, even at the expense of a few more declarations.

As a (not so good) example, consider the following declaration:

------------------------------------------------------------
-- procedure Create
------------------------------------------------------------
--
   procedure Create
              (The_Subscriber: in out Subscriber.Handle;
               With_Name     : in out Subscriber.Name);
--
-- Purpose: This procedure creates a subscriber with a given
-- name. 
--
-- Parameters: 
-     The_Subscriber    :mode in out, type Subscriber.Handle
-               It is the handle to the created subscriber
-     With_Name         :mode in, type Subscriber.Name
-               The name of the subscriber to be created.
-               The syntax of the name is
--                 <letter> { <letter> | <digit> }
-- Exceptions:
--    Subscriber.Collection_Overflow when there is no more
--    space to create a new subscriber
--    Subscriber.Invalid_Name when the name is blank or
--    malformed
--
-------------------------------------------- end Create ----      

Several points can be made about this example.

  • There is much redundancy:
- Procedure Create: If the name needs to be changed, there are several places to change it; consistent changes to the comment will not be enforced by the compiler.
- Parameters, with their name, mode, and type, need not be repeated in comments.
- Good names chosen for each Ada entity involved here make purpose and parameter explanations redundant. Note that this is true for a simple subprogram as shown above. A more complex subprogram still requires explanation of purpose and parameters.
  • The frame adds too much noise and hides the key item: the procedure declaration. Also, the vertical border on the right looks nice initially but makes modification painful, and it usually ends up totally misaligned and with holes after a few years of maintenance.
  • Contrarily, it is necessary to document which exceptions are raised here, since it is not obvious from just reading the specification. However, the precise meaning of each exception should be left attached to the exception declarations themselves.
  • Preconditions and postconditions on the parameters should be expressed, particularly stressing relationships between parameters. These should not duplicate information found elsewhere, such as the syntax of valid names, which should be expressed at only one point.

In this case, the following more concise and useful version is preferred:

procedure Create (The_Subscriber : in out Subscriber.Handle;
                  With_Name      : in    Subscriber.Name);--
-­Raises Subscriber.Collection_Overflow.
-­Raises Subscriber.Invalid_Name when the name is 
­­blank or malformed (see syntax description 
­­attached to  declaration of type Subscriber.Name).      

Guidelines for the Use of Comments

Comments should be placed near the code they are associated with, with the same indentation, and attached to that code-that is, with blank comment line(s) visually tying the block of comments to the Ada construct:

procedure First_One;
--
-- This comment relates to First_One.
-- But this comment is for Second_One.
-- 
procedure Second_One (Times : Natural);      

Use blank lines to separate related blocks of source code (comments and code) rather than heavy comment lines such as:

-------------------------------------------------------------      

or:

--===========================================================      

Use empty comments, rather than empty lines, within a single comment block to separate paragraphs:

-- Some explanation here that needs to be continued in a
-- subsequent paragraph.
--
-- The empty comment line above makes it clear that we 
-- are dealing with a single comment block.      

Although comments can be placed above or below the Ada construct(s) to which they are related, place comments such as a section title or a major piece of information that applies to several Ada constructs above the construct(s). Place comments that are remarks or additional information below the Ada construct to which they apply.

Group comments at the beginning of the Ada construct, using the whole width of the page. Avoid comments on the same line as an Ada construct. These comments often become misaligned. Such comments are tolerated, however, in descriptions of each element in long declarations, such as enumeration type literals.

Use a small hierarchy of standard blocks of comments for section titles, but only in very large Ada units (>200 declarations or statements):

--===========================================================
--
­­               MAJOR TITLE HERE
--
--===========================================================


-------------------------------------------------------------
­­               Minor Title Here
-------------------------------------------------------------


­­             --------------------
­­               Subsection Header
­­             --------------------      

Put more blank lines above such title comments than below-for example, two lines before and one line after. This visually associates the title with the following text.

Avoid the use of headers containing information such as author, phone numbers, dates of creation and modification, and location of unit (or filename), because this information rapidly becomes obsolete. Place ownership copyright notices at the end of the unit, especially when using the Rational Environment. When accessing the source of a package specification-by pressing [Definition] on the Rational Environment, for instance-the user does not want to have to scroll through two or three pages of text that is not useful for the understanding of the program, and/or text that does not carry any program information at all, such as a copyright notice. Avoid the use of vertical bars or closed frames or boxes, which just add visual noise and are difficult to keep consistent. Use Rational CMVC notes (or some other form of software development files) to keep unit history.

Do not replicate information normally found elsewhere; provide a pointer to the information.

Use Ada wherever possible, rather than a comment. To achieve this, you can use better names, extra temporary variables, qualification, renaming, subtypes, static expressions, and attributes, all of which do not affect the generated code (at least with a good compiler). You can also use small, inlined predicate functions and split the code into several parameterless procedures, whose names provide titles for several discrete sections of code.

Examples:

Replace:

exit when Su.Locate (Ch, Str) /= 0; 
-- Exit search loop when found it.      

with:

Search_Loop : loop

Found_It := Su.Locate (Ch, Str) /= 0;

exit Search_Loop when Found_It

end Search_Loop;

Replace:

if Value < 'A' or else Value > 'Z' then 
-- If not in uppercase letters.      

with:

subtype Uppercase_Letters is Character range 'A' .. 'Z';
if Value not in Uppercase_Letters then ...      

Replace:

X := Green;         -- This is the Green from 
                    -- Status, not from Color.
raise Fatal_Error;  -- From package Outer_Scope.
delay 384.0;        -- Equal to 6 minutes and 24 
                    -- seconds.      

with:

The_Status := Green;      

or:

X := Status'(Green);
raise Outer_Scope.Fatal_Error;
delay 6.0 * Minute + 24.0 * Second;      

Replace:

if Is_Valid (The_Table (Index).Descriptor(Rank).all) then
-- This is the current value for the iteration; if it is 
-- valid we append to the list it contains.
   Append (Item,           To_List => The_Table (Index).Descriptor(Rank).Ptr);|      

with:

declare
    Current_Rank : Lists.List renames The_Table 
                    (Index).Descriptor (Rank);
begin
    if Is_Valid (Current_Rank.all) then
        Append (Item, To_List => Current_Rank.Ptr);
    end if;
end;      

Take care with style, syntax, and spelling in comments. Do not use a telegraphic, cryptic style. Use a spelling checker. (On the Rational Environment invoke Speller.Check_Image).

Do not use accented letters or other non-English characters. Non-English characters may be supported on some development systems and on some Ada compilers in comments only, according to Ada Issue AI-339. But this is not portable, and it is likely to fail on other systems.

For subprograms, document at least:

  • the purpose of the subprogram, but only if it is not obvious from the name
  • which exceptions are raised and under which conditions
  • preconditions and postconditions on parameters, if any
  • additional data accessed, especially if it is modified; this includes especially functions that have side-effects
  • any limitations or additional information needed to properly use the subprogram.

For types and objects, document any invariant, or additional constraints that cannot be expressed in Ada.

Avoid repetitions in comments. For example, the purpose section should be a brief answer to the question "what does this do?" and not "how is it done?" The overview should be a brief presentation of the design. The description should not describe the algorithms used, but should instead explain how the package is to be used.

The Data_Structure and algorithm section should contain enough information to help understand the main implementation strategy (so that the package can be used properly), but does not have to provide all implementation details, or information that is not relevant to the proper use of this package.


Chapter 5

Naming Conventions

General

Choosing good names to designate Ada entities (program units, types, subtypes, objects, literals, exceptions) is one of the most delicate issues to address in all software applications. In medium-to-large applications, another problem arises: conflicts in names, or rather the difficulty in finding enough synonyms to designate distinct but similar notions about the same real-world concept (or to name a type, subtype, object, parameter). Here the rule not to use "use" clauses (or only in highly restricted conditions) can be exploited. In many situations, this will permit the shortening of a name and the reuse of the same descriptive words without risk of confusion.

Choose clear, legible, meaningful names.

Unlike many other programming languages, Ada does not limit the length of identifiers to 6, 8, or 15 characters. Speed of typing is not an acceptable justification for short names. One-letter identifiers are usually an indication of poor choice or laziness. There might be a few exceptions, such as using E for the base of the natural logarithms, Pi, or a handful of other well-recognized cases.

Separate various words of a name by an underscore:

Is_Name_Valid rather than IsNameValid

Use full names rather than abbreviations.

Use only project-approved abbreviations

If abbreviations are used, either they must be very common to the application domain (for example, FFT for Fast Fourier Transform) or they should be taken out of a project-level list of recognized abbreviations. Otherwise, it is very likely that similar but not quite identical abbreviations will occur here and there, introducing confusion and errors later (for example, Track_Identification being abbreviated Tr_Id, Trck_Id, Tr_Iden, Trid, Tid, Tr_Ident, and so on).

Use sparingly suffixes indicating category of Ada construct. They do not improve legibility.

Suffixes by category of Ada entities, such as _Package for packages, _Error for exceptions, _Type for type, and _Param for subprogram parameters are usually not very effective for the process of reading and understanding the code. This is even worse with suffixes such as _Array, _Record, and _Function. Both the Ada compiler and the human reader can distinguish an exception from a subprogram by the context: it is obvious that only an exception name can appear in a raise statement or in an exception handler. Such suffixes are useful in the following limited situations:

  • When the choice of appropriate words is very limited; give the best name to the object and use a suffix for the type
  • For generic units, which can always be suffixed by _Generic, thus allowing the use of the same name without the suffix for some or most of the instantiations
  • When it represents an application-domain concept: Aircraft_Type
  • When important design decisions need to be visible:

Generic formal type suffixed by _Constrained

Access type suffixed by _Pointer or other form of indirect reference: by _Handle, or _Reference

Subprogram hiding a potentially blocking entry call by _Or_Wait

Express names so that they look nice from the usage point of view.

Try to think about the context in which an exported entity will be used, and choose the name from that point of view. An entity is declared once and used many times. This is especially true for subprogram names and their parameters: the resulting calls, using named associations, should be as close as possible to natural language. Remember that the absence of use clauses will make compulsory the qualified name of most declared entities. Good compromises have to be found for generic formal parameters, which may be used more in the generic unit than on its client side, but definitely give preference to a nice look on the client side for subprogram formal parameters.

Use English words and spell them correctly.

Language mixture (for example, French and English) makes the code difficult to read and sometimes introduces ambiguities in the meaning of identifiers. Since Ada keywords are already in English, English words are required. American spelling is preferred, in order to be able to use the built-in spelling checker on the Rational Environment.

Do not redefine any entity from package Standard. This is absolutely forbidden.

To do so leads to confusion and dramatic mistakes. The rule could be extended to other predefined library units: Calendar, System. And this includes the identifier Standard itself.

Avoid the redefinition of identifiers from other predefined packages, such as System, or Calendar.

Do not use as identifiers: Wide_Character and Wide_String which will be introduced in package Standard in Ada 95. Do not introduce a compilation unit named Ada.

Do not use as identifiers the words: abstract, aliased,protected, requeue, tagged and until, which will become keywords in Ada 95.

Some naming suggestions for various Ada entities follow. A generally "object-flavored" style of design is assumed. See Annex A for further explanations.

Packages

When a package introduces some object class, give it the name of the object class, usually a common noun in singular form, with the suffix _Generic if necessary (that is, if a parameterized class is defined). Use the plural form only if the objects always come in groups. For example:

package Text is
package Line is
package Mailbox is
package Message is
package Attributes is
package Subscriber is
package List_Generic is      

When a package specifies an interface or some grouping of functionality, and does not relate to an object, express this in the name:

package Low_Layer_Interface is
package Math_Definitions is      

When a "logical" package needs to be expressed as several packages, using a flat decomposition, use suffixes drawn from a list agreed upon at the project level. A logical package Mailbox, for example, could be implemented with:

package Mailbox_Definitions is
package Mailbox_Exceptions is
package Mailbox_Io is
package Mailbox_Utilities is
package Mailbox_Implementation is
package Mailbox_Main is      

Other acceptable suffixes are:

_Test_Support 
_Test_Main 
_Log 
_Hidden_Definitions 
_Maintenance 
_Debug      

Types

In a package defining an object class, use:

type Object is ...      

 

when copy semantics is implied-that is, when the type is instantiable and some form of assignment is feasible. Note that the name of the class should not be repeated in the identifier, since it will always be used in its fully qualified form:

Mailbox.Object
Line.Object      

When shared semantics is implied-that is, the type is implemented with access values (or some other form of indirection), and assignment, if available, does not copy the object-indicate this fact by using:

UL>

  • type Handle is for an indirect reference
  • type Reference is as a possible alternate

 

The elements are used as suffixes when their use alone, prefixed by the package name, is unclear or ambiguous.

When multiple objects are implied, use:

  • type Set is when uniqueness of elements is implied
  • type List is when some ordering is implied
  • type Collection is when neither set nor list semantics is implied
  • type Iterator is when the primitive Initialize, Value_Of, Next, Is_Done are provided
    (cf. section 6.5).

For some string designation of the object, use:

type Name is

The qualified name of the type should also be used throughout the defining package, for better legibility. On the Rational Environment, this also leads to better behavior when using the [Complete] function on a subprogram call.

For example, note the full name Subscriber.Object below:

package Subscriber is
    type Object is private;
    type Handle is access Subscriber.Object;
    subtype Name is String;
    package List is new List_Generic (Subscriber.Handle);
    Master_List : Subscriber.List.Handle;
    procedure Create (The_Handle : out Subscriber.Handle;
                      With_Name  : in  Subscriber.Name);
    procedure Append (The_Subscriber : in     Subscriber.Handle;
                      To_List        : in out Subscriber.List.Handle);
    function Name_Of (The_Subscriber : Subscriber.Handle) return
            Subscriber.Name;
    ...
private
    type Object is
        record
            The_Name : Subscriber.Name (1..20);
                    ...
end Subscriber;    

In other circumstances, use nouns or qualifier+noun for the name of a type. You might use the plural form for the type, leaving the singular for objects (variables):

type Point is record ...
type Hidden_Attributes is ( ...
type Boxes is array ...    

For enumeration types, use Mode, Kind, Code, and so on, alone or as a suffix.

For array types, the suffix _Table can be used when the simple name is already used for the component type. Use names or suffixes like _Set and _List only when the array is maintained with the implied semantics. Reserve _Vector and _Matrix for the corresponding mathematical concepts.

Since singular task objects will be avoided (for reasons explained later), a task type should be introduced even when there is only one object of that type. This is a case where a simple-minded suffix strategy such as _Type is satisfactory:

task type Listener_Type is ...
for Listener_Type'Storage_Size use ...
Listener : Listener_Type;    

Similarly, when a conflict exists between using a noun (or noun phrase) for the name of the type, or in several places for the name of the object or parameter, then suffix that noun with _Kind for the type and keep the simple noun for the object:

type Status_Kind is (None, Normal, Urgent, Red);
Status : Status_Kind := None;    

Or, for things that always come in multiples, use the plural form for the type.

Since access types have inherent dangers, the user should be made aware of them. They are called Pointer in general. Use the suffix _pointer if the name alone is ambiguous. As an alternate _Access is possible. ;

Sometimes using a nested subpackage to introduce a secondary abstraction simplifies naming:

package Subscriber is    ...
    package Status is
        type Kind is (Ok, Deleted, Incomplete, Suspended, 
                      Privileged);
        function Set (The_Status    : Subscriber.Status.Kind;
                      To_Subscriber : Subscriber.Handle);
    end Status;
    ...    

Exceptions

Since exceptions must be used only to handle error situations, use a noun or a noun phrase that clearly conveys a negative idea:

Overflow, Threshold_Exceeded, Bad_Initial_Value    

When defined in a class package, it is useless for the identifier to contain the name of the class-for example, Bad_Initial_Subscriber_Value-since the exception will always be used as Subscriber.Bad_Initial_Value.

Use one of the words Bad, Incomplete, Invalid, Wrong, Missing, or Illegal as part of the name rather than systematically using Error, which does not convey specific information:

Illegal_Data, Incomplete_Data    

Subprograms

Use verbs for procedures (and task entries). Use nouns with the attributes or characteristics of the object class for functions. Use adjectives (or past participles) for functions returning a Boolean (predicates). s

Subscriber.Create
Subscriber.Destroy
Subscriber.List.Append
Subscriber.First_Name          -- Returns a string.
Subscriber.Creation_Date       -- Returns a date.
Subscriber.List.Next
Subscriber.Deleted             -- Returns a Boolean.
Subscriber.Unavailable         -- Returns a Boolean.
Subscriber.Remote    

For predicates, it may be useful in some cases to add the prefix Is_ or Has_ before a noun; be accurate and consistent with respect to tense:

function Has_First_Name ...
function Is_Administrator ...
function Is_First...
function Was_Deleted ...    

This is useful when the simple name is already used as a type name or an enumeration literal.

Use predicates in the positive form, i.e., they should not contain "Not_".

For common operations, consistently use verbs drawn from a project list of choices (list to be expanded as we gain knowledge of the system):

Create
Delete
Destroy
Initialize
Append
Revert
Commit
Show, Display    

Use positive names for predicate functions and boolean parameters. Using negative names can create double negations (e.g., Not Is_Not_Found), and can make the code more difficult to read.

function Is_Not_Valid (...) return Boolean
procedure Find_Client (With_The_Name : in  Name;
                       Not_Found     : out Boolean)    

should be defined as:

function Is_Valid (...) return Boolean;
procedure Find_Client (With_The_Name: in Name;
                       Found: out Boolean)    

which lets the client negate their expression as required (there is no runtime penalty for doing so):

if not Is_Valid (...) then ....    

In some cases, a negative predicate can also be made positive without changing its semantics by using an antonym, such as "Is_Invalid" instead of "Is_Not_Valid." However, positive names are more readable: "Is_Valid" is easier to understand than "not Is_Invalid."

Use the same word when the same general meaning is implied, rather than trying to find synonyms or variations. Overloading therefore is encouraged to enhance uniformity, in keeping with the principle of minimal surprise.

If subprograms are used as "skins" or "wrappers" for entry calls, it may be useful that the name reflects this fact by suffixing the verb with _Or_Wait or by having a phrase such as Wait_For_ followed by a noun:

Subscriber.Get_Reply_Or_Wait
Subscriber.Wait_For_Reply    

Some operations should always be consistently defined using the same names:

For type conversions to and from strings, the symmetrical functions:

    function Image and function Value    

For type conversions to and from some low-level representation (such as Byte_String for data interchange):

    procedure Read and Write    

For allocated data:

    function Allocate (rather than Create)
    function Destroy (or Release, to express that the object will disappear)    

When this is done systematically, using consistent naming, type composition is made much easier.

For active iterators, the following primitives must always be defined:

Initialize
Next
Is_Done
Value_Of    

and, if feasible, Reset. If several iterator types are introduced in the same scope, these primitives should be overloaded rather than introducing a distinct set of identifiers for each iterator. Cf. [BOO87].

When using Ada predefined attributes as function names, make sure that they are used with the same general semantics: 'First, 'Last, 'Length, 'Image, 'Value, and so on. Note that several attributes (for example, 'Range and 'Delta) cannot be used as function names because they are reserved words.

Objects and Subprogram (or Entry) Parameters

To indicate uniqueness, or to show that this entity is the main focus of the action, prefix the object or parameter name with The_ or This_. To indicate a side, temporary, auxiliary object, prefix it with A_ or Current_:

procedure Change_Name (The_Subscriber : in Subscriber.Handle;
                       The_Name       : in Subscriber.Name );
declare
    A_Subscriber : Subscriber.Handle := Subscriber.First;
begin
    ...
    A_Subscriber := Subscriber.Next (The_Subscriber);
end;    

For Boolean objects, use a predicate clause, with the positive form:

Found_It
Is_Available    

 

but:

Is_Not_Available must be avoided.

For task objects, use a noun or noun phrase that implies an active entity:

Listener
Resource_Manager
Terminal_Driver    

For parameters, prefixing the class name or some characteristic noun with a preposition also adds legibility, especially on the caller's side when named association is used. Other useful prefixes for auxiliary parameters have the form Using_ or, in the case of an in out parameter that is affected as some secondary effect, Modifying_:

procedure Update (The_List     : in out Subscriber.List.Handle;
                  With_Id      : in     Subscriber.Identification;
                  On_Structure : in out Structure;
                  For_Value    : in     Value);
procedure Change (The_Object   : in out Object;
                  Using_Object : in     Object);    

The order in which parameters are defined is also very important from the caller's point of view:

  • First define the non-defaulted parameters (which therefore includes all out and in out parameters) in order of decreasing importance. For an operation of a class, this starts by the object being the main focus of the operation.
  • Then define the parameters that have default values, with the most likely to be modified first.

This permits taking advantage of defaults without having to use named association for the main parameter(s).

The mode "in" must be explicitly indicated, even in functions.

Generic Units

Pick the best name you would use for a non-generic version: class name for a package or transitive verb (or verb phrase) for a procedure (see above) and suffix it with _Generic.

For generic formal types, when the generic package defines some abstract data structure, use Item or Element for the generic formal and Structure, or some other more appropriate noun, for the exported abstraction.

For passive iterators, use a verb such as Apply, Scan, Traverse, Process, or Iterate in the identifier:

generic
		with procedure Act	(Upon : in out Element);
procedure Iterate_Generic	(Upon : in out Structure);    

Names of generic formal parameters cannot be homographs.

generic
    type Foo is private;
    type Bar is private;
    with function Image (X : Foo) return String;
    with function Image (X : Bar) return String;
package Some_Generic is ...    

shall be replaced by:

generic
    type Foo is private;
    type Bar is private;
    with function Foo_Image (X : Foo) return String;
    with function Bar_Image (X : Bar) return String;
package Some_Generic is ...    

If needed, the generic formal parameters can be renamed in the generic unit:

function Image (Item : Foo) return String Renames Foo_Image;
function Image (Item : Bar) return String Renames Bar_Image;    

Naming Strategies for Subsystems

When a large system is partitioned into Rational subsystems (or another form of interconnected program libraries), it is useful to define a naming strategy that allows:

Avoidance of name conflicts

In a system that comprises several hundred objects and sub-objects, some name conflicts are likely to occur at the library-unit level, and programmers will be short of synonyms for some very useful names like Utilities, Support, Definitions, and so on.

Easy location of Ada entities

Using browsing facilities on the Rational host, finding where an entity is defined is an easy task, but when code is ported to a target and uses target tools (debuggers, testing tools, and so on), the location of a procedure Utilities.Get among 2,000 units in 100 subsystems may be quite a challenge for a newcomer to the project.

Prefix library-level unit names with the four-letter abbreviation of the subsystem in which it is contained.

The list of subsystems can be found in the Software Architecture Document (SAD). Exclude from this rule libraries of highly reusable components that are likely to be reused across numerous projects, COTS products, and standard units.

Example:

Comm Communication

Dbms Database management

Disp Displays

Math Mathematical packages

Driv Drivers

For example, all library units exported from subsystem Disp will be prefixed with Disp_, allowing the team or company in charge of Disp to have otherwise complete freedom of naming. If both DBMS and Disp need to introduce an object class named Subscriber, this will result in packages such as :

Disp_Subscriber
Disp_Subscriber_Utilities
Disp_Subscriber_Defs
Dbms_Subscriber
Dbms_Subscriber_Interface
Dbms_Subscriber_Defs    

Chapter 6

Declarations of Types, Objects, and Program Units

Ada's strong typing facility will be used to prevent mixing of different types. Conceptually different types must be realized as different user-defined types. Subtypes should be used to improve program readability and to enhance the effectiveness of the run-time checks generated by the compiler.

Enumeration Types

Whenever possible, introduce into the enumeration some extra literal value representing uninitialized, invalid, or no value at all:

type Mode  is (Undefined, Circular, Sector, Impulse);
type Error is (None, Overflow, Invalid_Input_Value,Illformed_Name);    

This will support the rules for systematically initializing objects. Put this literal at the beginning rather than at the end of the list, to ease maintenance and to allow contiguous subranges of valid values such as:

subtype Actual_Error is Error range Overflow .. Error'Last;    

Numeric Types

Avoid the use of predefined numeric types.

When a high degree of portability and reusability is the objective, or when control is needed over the memory space occupied by numeric objects, then predefined numeric types (from package Standard) must not be used. The reason for this requirement is that the characteristics of the predefined types Integer and Float are (deliberately) unspecified in the Reference Manual for the Ada Programming Language [ISO87].

A first systematic strategy is to introduce project-specific numeric types-in a package System_Types, for instance-with names that carry an indication of the accuracy or memory size:

package System_Types is
        type Byte is range -128 .. 127;
        type Integer16 is range -32568 .. 32567;
        type Integer32 is range ...
        type Float6 is digits 6; 
        type Float13 is digits 13;
...
end System_Types;    

Do not redefine standard types (types from package Standard).

Do not specify which base type they should be derived from; let the compiler choose. This following example is bad:

type Byte is new Integer range -128 .. 127;    

Float6 is a better name than Float32, even if on most machines 32-bit floats will achieve 6 digits of accuracy.

In the various parts of the project, derive types with more meaningful names than those in Baty_System_Types. Some of the most accurate types could be made private to support an eventual port to a target with limited precision support.

This strategy is to be used when:

  • several types must be correlated
  • we want to get some useful operations for the type by derivation, such as conversions to external formats, or additional arithmetic or mathematic functions.

If this is not the case, then another simpler strategy is to always define new types, specifying the requested range and accuracy, but never specifying the base type they should be derived from. For example, declare:

type Counter is range 0 .. 100;
type Length is digits 5;    

rather than:

type Counter is new Integer range 1..100; -- could be 64 bits
type Length is new Float digits 5; -- could be digits 13    

This second strategy forces the programmer to think of the precise bounds and accuracy each type requires, rather than arbitrarily selecting a certain number of bits. Be aware, however, that if the range is not identical to that of a base type, systematic range checks will be applied by the compiler-for example, for type Counter above, if the base type is a 32-bit integer.

If the range checks are becoming a problem, one way to avoid them is to declare:

type Counter_Min_Range is range 0 .. 10_000;
type Counter is range Counter_Min_Range'Base'First .. Counter_Min_Range'Base'Last;    

Avoid standard types leaking into the code through constructs such as loops, index ranges, and so on.

Subtypes of the predefined numeric types are used only in the following circumstances:

  • subtype Positive to index objects of type String
  • type Integer as exponent in integer exponentiation, and in several standard elementary functions,
  • in arithmetic expressions, for scaling real values.

Example:

for I in 1 .. 100 loop ...     
-- I is of type Standard.Integer
type A is array (0 .. 15) of Boolean; 
-- index is Standard.Integer.    

Use instead the form: Some_Integer range L .. H

for I in Counter range 1 .. 100 loop ...
type A is array (Byte range 0 .. 15) of Boolean;    

Do not try to implement unsigned types.

Integer types with unsigned arithmetic do not exist in Ada. Under the language definition, all integer types are derived indirectly or not from the predefined types, and these in turn must be symmetrical about zero.

Real Types

For portability, rely only on real types having values in the ranges:

[-F'Large .. -F'Small]  [0.0]  [F'Small .. F'Large]    

Be aware that F'Last and F'First may not be model numbers and may even not be in any model interval. The relative location of F'Last and F'Large depends on the type definition and the underlying hardware. One particularly nasty example is the case where 'Last of a fixed-point type does not belong to the type, as in:

type FF is delta 1.0 range -8.0 .. 8.0;    

where, according to a strict reading of the Ada Reference Manual 3.5.9(6), FF'Last = 8.0 cannot belong to the type.

To represent large or small real numbers, use attributes 'Large or 'Small (and their negative counterparts), not 'First and 'Last, as would be done for integer types.

For floating-point types, use only <= and >=, never =, <, >, /=.

The semantics of absolute comparison are ill-defined (equality of representation and not equality within the required degree of accuracy). For example, X < Y may not yield the same result as: not (X >= Y). Tests for equality, A = B, should be expressed as:

abs (A - B) <= abs(A)*F'Epsilon    

To improve readability and maintainability, consider providing an Equal operator that encapsulates the above expression.

Note also that the simpler expression:

abs (A - B) <= F'Small    

is valid only for small values of A and B, and therefore is not generally recommended.

Avoid any reference to the predefined exception Numeric_Error. A binding interpretation of the Ada Board has made all cases that used to raise Numeric_Error now raise Constraint_Error. The exception Numeric_Error is obsolete in Ada 95.

If Numeric_Error is still raised by the implementation (this is the case of the Rational native compiler), then always check for Constraint_Error together with Numeric_Error in the same alternative in an exception handler:

when Numeric_Error | Constraint_Error => ...    

Be wary of underflow.

Underflow is not detected in Ada. The result is 0.0 and no exception is raised. Note that a check for underflow can be explicitly achieved by testing the result of a multiplication or division against 0.0, when none of the operands is 0.0. Note also that you can implement your own operators to automatically perform such checking, although at some cost in efficiency.

The use of fixed-point types is restricted.

Use floating-point types whenever possible. Uneven implementation of fixed-point types across an Ada implementation causes portability problems.

For fixed-point types, 'Small should be equal to 'Delta.

The code should specify this. The fact that the default choice for 'Small is a power of 2 leads to all kinds of problems. One way to make the choice clear is to write:

Fx_Delta : constant := 0.01;
type FX is delta Fx_Delta range L .. H;
for FX'Small use Fx_Delta;    

If length clauses for fixed-point types are not supported, the only way to obey this rule is to specify explicitly a 'Delta that is a power of 2. Subtypes can have a 'Small different from 'Delta (the rule applies only to the type definition, or "first named subtype" in the terminology of the Ada Reference Manual).

Record Types

Wherever possible, provide simple, static initial values for the components of a record type (often values such as 'First or 'Last can be used).

But do not apply this to discriminants. The rules of the language are such that discriminants always have values. Mutable records (that is, records with default values for discriminants) should be introduced only when mutability is a wanted characteristic. Otherwise, mutable records introduce extra overhead in memory space (often the largest variant is allocated) and time (variant checks are more complex to achieve).

Avoid function calls in default initial values of any component, since this may lead to an "access before elaboration" error (see "Program Structure and Compilation Issues").

For mutable records (records whose discriminants have default values), if a discriminant is used in the dimensioning of some other component, specify it to be of a reasonable small range.

Example:

type Record_Type (D : Integer := 0) is 
        record 
            S : String (1 .. D);
        end record;
A_Record : Record_Type;    

is likely to raise a Storage_Error on most implementations. Specify a more reasonable range for the subtype of the discriminant D.

Do not assume anything about the physical layout of records.

Especially, and unlike other programming languages, components need not be laid out in the order given in the definition.

Access Types

Restrict the use of access types.

This is especially true for applications that are meant to run permanently on small machines without virtual memory. Access types are dangerous, since small programming mistakes can lead to storage exhaustion and, even with good programming, can fragment memory. Access types are also slower. The use of access types must be part of a project wide strategy, and collections, their size, and points of allocation and deallocation should be tracked. To make clients of an abstraction aware that access values are manipulated, the name chosen should indicate this: Pointer or a name suffixed by _Pointer.

Allocate collections during program elaboration, and systematically specify the size of each collection.

The value given (in storage units) can be static or computed dynamically (read from a file, for instance). The rationale for this rule is that the program should fail immediately at startup, rather than die mysteriously N days later. Generic packages may provide for this with an additional generic formal specifying the size.

Note that there is often some overhead for each allocated object: it may be that the runtimes on the target system allocate some additional information with each memory chunk for internal housekeeping. So, to store N objects of size M storage units, it may be necessary to allocate more than N * M storage units for the collection-for example, N * (M + K). Obtain the value of this overhead K from Appendix F of [ISO87] or by conducting experiments.

Encapsulate the use of allocators (Ada primitive new) and release. If feasible, manage an internal free list, rather than relying on Unchecked_Deallocation.

If an access type is used to implement some recursive data structure, then it is very likely to access a record type that has (as one component) that same access type. This allows recycling of free cells by chaining them in a free list with no additional space overhead (other than the pointer to the head of the list).

Handle explicitly Storage_Error exceptions raised by new, and reexport a more meaningful exception, indicating exhaustion of the collection's maximum storage size.

Having a single point of allocation and deallocation also allows easier tracing and debugging in case of a problem.

Use deallocation only on allocated cells of the same size (hence same discriminants).

This is important in order to avoid memory fragmentation. Unchecked_Deallocation is very unlikely to provide a memory-compaction service. You may want to check whether the runtime system provides coalescing of adjacent released blocks.

P>Systematically provide a Destroy (or Free, or Release) primitive with access types.

This is especially important for abstract data types implemented with access types, and it should be done systematically to achieve composability of multiple such types.

Release objects systematically.

Try to map the calls to allocation and deallocation to make sure that all allocated data is deallocated. Try to deallocate data in the same scope in which it was allocated. Remember to deallocate also when exceptions occur. Note that this is one case for using a when others alternative, ending with a raise statement.

The preferred strategy is to apply the pattern: Get-Use-Release. The programmer Gets the objects (which creates some dynamic data structure), then it Uses it, then it must Release it. Make sure that the three operations are clearly identified in the code, and that the release is done on all possible exits of the frame, including by exception.

Be careful to deallocate the temporary composite data structures which might be contained in records.

Example:

type Object is
record
    Field1: Some_Numeric;
    Field2: Some_String;
    Field3: Some_Unbounded_List;
end record;    

where 'Some_Unbounded_List' is a composite linked structure, that is, it is composed of a number of objects linked together. Consider now a typical attribute function, written as:

function Some_Attribute_Of(The_Object: Object_Handle) return 
Boolean is Temp_Object: The_Object;
begin
    Temp_Object := Read(The_Object);
    return Temp_Object.Field1 < Some_Value;
end Some_Attribute_Of;    

The composite structure implicitly created in the heap when the object is read into Temp_Object is never deallocated, but is now unreachable. This is a memory leak. The proper solution is to implement a Get-Use-Release paradigm for such expensive structures. In other words, your client should Get the object first, then Use it as needed, then Release it:

procedure Get (The_Object  : out Object;
               With_Handle : in  Object_Handle);
function Some_Attribute_Of(The_Object : Object) 
                        return Some_Value;
function Other_Attribute_Of(The_Object	: Object) 
                        return Some_Value;
...
procedure Release(The_Object: in out Object);    

The client code might look like this:

 declare
    My_Object: Object;
begin
    Get (My_Object, With_Handle => My_Handle);
    ...
    Do_Something
      (The_Value => Some_Attribute_Of(My_Object));
      ...
    Release(My_Object);
end;    

Private Types

Declare types as private whenever it is necessary to hide implementation details.

Implementation details need to be hidden with a private type when:

  • Some internal consistency in the complete type must be maintained.
  • The objects of the type are not monolithic objects (that is, are not represented as a single contiguous segment of memory designated by one single name).
  • Many auxiliary types that should not be exported need to be defined.
  • Some of the predefined or intrinsic operations need be altered-for example, defining a type Angle where all arithmetic operations return a value in [0, 2].
  • The accuracy of the corresponding numeric type is not likely to be achieved directly on all potential targets.

In the Rational Environment, private types, in conjunction with closed private parts and subsystems, greatly reduce the impact of an eventual interface design change.

In contradiction to so-called "pure" object-oriented programming, do not use private types when the corresponding complete type is the best possible abstraction. Be pragmatic; ask if making the type private adds anything.

For example, a mathematical vector is better represented as an array, or a point in a plane as a record, than as a private type:

type Vector is array (Positive range <>) of Float;
Type Point is 
    record
        X, Y : Float := Float'Large;
    end record;    

Array indexing, record component selection, and aggregate notation will be far more legible (and eventually more efficient) than a series of subprogram calls, as would be required were the type unnecessarily private.

Declare private types as limited when default assignment or comparison of the actual objects and values is meaningless, non-intuitive, or impossible.

This is the case when:

  • the complete type itself contains a limited component
  • the complete type is not monolithic-for example: recursive data types implemented with access values.

A limited private type should be self-initializing.

An object declaration of such a type must receive a reasonable initial value, since generally it will not be feasible to assign a later one, without risk of raising some exception during a subprogram call.

Whenever feasible or meaningful, provide for limited types a Copy (or Assign) procedure and a Destroy procedure.

When designing a generic's formal types, specify limited private types as long as equality or assignment is not required internally, for greater usability of the corresponding generic unit.

In line with the previous rule, you might then import a Copy and a Destroy generic formal procedure and an Are_Equal predicate, if meaningful.

For generic formal private types, indicate in the specification whether the corresponding actual must be constrained or not.

This can be achieved by a naming convention and/or comment:

generic
    --Must be constrained.
    type Constrained_Element is limited private;
package ...    

or by using the Rational-defined pragma Must_Be_Constrained:

generic
    type Element is limited private;
    pragma Must_Be_Constrained (Element);
package ...    

Derived Types

Remember that deriving a type also derives all the subprograms that are declared in the same declarative part as the parent type: the derivable subprograms. It is therefore useless to redefine them all as skins in the declarative part of the derived type. But generic subprograms are not derivable and it may be necessary to redefine them as skins.

Example:

package Base is
    type Foo is
        record
            ...
        end record;
    procedure Put(Item: Foo);
    function Value(Of_The_Image: String) return Foo;
end Base;
with Base;
package Client is
    type Bar is new Foo;
    -- At this point, the following declarations are 
    -- implicitly made:
    -- 
    -- function "="(L,R: Bar) return Boolean;
    -- 
    -- procedure Put(Item: bar);
    -- function Value(Of_The_Image: String) return Bar;
    -- 
end Client;    

It is therefore not necessary to redefine these operations as skins. Note, however, that generic subprograms (such as passive iterators) are not derived along with other operations, and must therefore be re-exported as skins. Subprograms defined elsewhere than the specification containing the base type declaration are also not derivable, and must also be re-exported as skins.

Object Declarations

Specify initial values in object declarations, unless the object is self-initializing or there is an implicit default initial value (for example, access types, task types, records with default values for nondiscriminant fields).

P>The value assigned must be a real, meaningful value, not just any valueof the type. If the actual initial value is available, such as for example one of the input parameters, then assign it. If it is not possible to compute a meaningful value, then consider declaring the object later, or assign any "nil" value if available.

The name "Nil" is meant as "Uninitialized" and it is used to declare constants that can be used as a "unusable but known value" that can be rejected in a controlled fashion by algorithms.

Whenever feasible, the Nil value should not be used for any other purpose than initialization, so that its appearance can always indicate an uninitialized variable error.

Note that it is not always possible to declare a Nil value for all types, especially modular types, such as an angle. In this case choose the less likely value.

Note that code to initialize large records may be costly, especially if the record has variants and if some initial value is nonstatic (or, more precisely, if the value cannot be computed at compile time). It is sometimes more efficient to elaborate once and for all an initial value (perhaps in the package defining the type) and assign it explicitly:

R : Some_Record := Initial_Value_For_Some_Record;    

Note:

Experience shows that uninitialized variables are one of the main sources of problems in porting code and one of the major sources of programming errors. This is aggravated when the development host tries to be "nice" to the programmer by providing default values for at least some of the objects (for example, type Integer on the Rational native compiler) or when the target system zeroes the memory before program loading (for example, on a DEC VAX). To achieve portability, always assume the worst.

Assigning an initial value in the declaration can be omitted when it is costly and when it is obvious that the object is assigned a value before being used.

Example:

procedure Schmoldu is
    Temp : Some_Very_Complex_Record_Type;
 -- initialized later
begin
    loop
        Temp := Some_Expression ...
        ...    

Avoid the use of literal values in the code.

Use constants (with a type) when the value defined is bound to a type. Otherwise, use named numbers, especially for all dimensionless values (pure values):

Earth_Radius : constant Meter := 6366190.7;   -- In meters.
Pi           : constant       := 3.141592653; -- No units.    

Define related constants with universal, static expressions:

Bytes_Per_Page :   constant := 512;
Pages_Per_Buffer : constant := 10;
Buffer_Size :      constant := Bytes_Per_Page * Pages_Per_Buffer;
Pi_Over_2   :      constant := Pi / 2.0;    

This takes advantage of the fact that these expressions must be computed exactly at compile time.

Do not declare objects with anonymous types (cf. Ada Reference Manual 3.3.1)

Maintainability is reduced, objects cannot be passed as parameters, and it often leads to type conflict errors.

Subprograms and Generic Units

Subprograms can be declared as procedures or functions; here are some general criteria that can be used to choose which form to declare.

Declare a function when:

  • you define an operator, and this operator is the most readable way to express the role of the subprogram
  • there is a well-defined "algebra" on this type (e.g., strings, arithmetic, geometry)
  • most of the calls are likely to be in expressions (other than a trivial expression such as Result := F (X);)
  • the body of the subprogram is small (less than 5 lines)
  • the type of the result is Boolean (calls are in while loops and if statements)
  • most of the uses are likely to be in declarative parts
  • you simply return an attribute of some private object
  • there are no side-effects; no error can occur.

Declare a procedure when:

  • there are many parameters
  • the call is most likely to be in a statement part
  • the result is a composite type that is likely to be very large
  • errors can occur.
  • When in doubt, or if there is a very close tie, declare a procedure.

Avoid giving default values to generic formal parameters used for sizing structures (tables, collections, etc.)

Write local procedures with as few side effects as possible, and functions with no side effects at all. Document the side effect.

Side effects are usually modifications of global variables, and may only be noticed when reading the body of the subprogram. The programmer may not be aware of side effects at the call site.

Passing in the required objects as parameters makes the code more robust, easier to understand and less dependent on its content.

This rule applies mainly to local subprograms: exported subprograms often require legitimate access to global variables in the package body.


Chapter 7

Expressions and Statements

Expressions

Use redundant parentheses to make compound expressions clearer.

The level of nesting of an expression is defined as the number of nested sets of parentheses required to evaluate an expression from left to right if the rules of operator precedence were ignored.

Limit the level of nesting of expressions to four.

Record aggregates should use named associations and should be qualified:

Subscriber.Descriptor'(Name    => Subscriber.Null_Name,
                       Mailbox => Mailbox.Nil,
                       Status  => Subscriber.Unknown,
                   ...);    

The use of a when others is forbidden for record aggregates.

This is because, in contrast to arrays, records are naturally heterogeneous structures, and uniform assignment therefore is unreasonable.

Use simple Boolean expressions in place of "if...then...else" statements for simple predicates:

PRE>function Is_In_Range(The_Value: Value; The_Range: Range) return Boolean is begin if The_Value >= The_Range.Min and The_Value <= The_Range.Max; then return True; end if; end Is_In_Range;

 

should be rewritten as:

function Is_In_Range(The_Value: Value; The_Range: Range)
     return Boolean is
begin
    return The_Value >= The_Range.Min 
        and The_Value <= The_Range.Max;
end Is_In_Range;    

Complex expressions containing two or more if statements should not be changed in this manner if it affects readability.

Statements

Loop statements should have names:

  • when they extend over more than 25 lines
  • when they are nested
  • when there is a meaningful name to designate what they perform
  • when the loop has no end:
Forever: loop
   ...
end loop Forever;    

When a loop has a name, any exit statement it contains should specify it.

Loops which require a completion test at the beginning should use the "while" loop form. Loops which require a completion test elsewhere should use the general form and an exit statement.

Minimize the number of exit statements in a loop.

In a "for" loop that iterates oven an array, use the 'Range attribute applied on the array object, rather than an explicit range or some other subtype.

Move any loop-independent code out of the loop. Although "code hoisting" is a common compiler optimization, it cannot be done when the invariant code makes calls to other compilation units.

Example:

World_Search:
while not World.Is_At_End(World_Iterator) loop
    ...
    Country_Search:
    while not Nation.Is_At_End(Country_Iterator) loop
    declare
        City_Map: constant City.Map := City.Map_Of
            (The_City => Nation.City_Of(Country_Iterator),
             In_Atlas => World.Country_Of(World_Iterator).Atlas);
    begin
        ...    

In the above code, the call to "World.Country_Of" is loop-independent (i.e., the country remains unchanged in the inner loop). However, in most cases, the compiler is prohibited from moving the call out of the loop, since the call may have side effects that can affect the program execution. The code will therefore execute unnecessarily each time through the loop.

The loop is more efficient and easier to understand and maintain if rewritten as:

Country_Search:
while not World.Is_At_End(World_Iterator) loop
    declare
        This_Country_Atlas: constant Nation.Atlas 
            := World.Country_Of
                    (World_Iterator).Atlas;
    begin
        ...
        City_Search:
        while not Nation.Is_At_End (The_City_Iterator) loop
            declare
                City.Map_Of (
                    The_City => Nation.City_Of
                                        (Country_Iterator),
                    In_Atlas => This_Country_Atlas );
            begin
                ...    

Subprogram and entry calls should use named associations.

However, if it is clear that the first (or only) parameter is the main focus of the operation (for example, a direct object of a transitive verb), the name can be omitted for this parameter only:

Subscriber.Delete (The_Subscriber => Old_Subscriber);    

where Subscriber.Delete is the transitive verb, and Old_Subscriber is the direct object. The following expressions without the named association The_Subscriber => Old_Subscriber are acceptable:

Subscriber.Delete	(Old_Subscriber);
Subscriber.Delete (Old_Subscriber, 
                   Update_Database  => True,
                   Expunge_Name_Set => False);
if Is_Administrator (Old_Subscriber) then ...    

There are also cases where the meaning of parameters is so obvious that named association would just degrade legibility. This is true, for instance, when all parameters are of the same type and mode and have no default values:

if Is_Equal (X, Y) then ...
Swap (U, B);    

A when others should not be used in case statements or in record type definitions (for variants).

Not using a when others will help during the maintenance phase by making these constructs invalid whenever the discrete type definition is modified, forcing the programmer to consider what should be done to handle he modification. However it is tolerated when the selector is a large integer range.

Use a case statement rather than a series of "elsif" when the branching condition is a discrete value.

Subprograms should have a single point of return.

Try to exit from subprograms at the end of the statement part. Functions should have a single return statement. Return statements sprinkled freely over a function body are akin to goto statements, making the code difficult to read and to maintain.

Procedures should have no return statements at all.

Multiple returns can be tolerated only in very small functions, when all returns can be seen simultaneously and when the code has a very regular structure:

function Get_Some_Attribute return Some_Type is
begin
    if Some_Condition then
        return This_Value;
    else
        return That_Other_Value;
    end if;
end Get_Some_Attribute;    

The use of goto statements is restricted.

In defense of the "goto" statement;, it should be noted that the syntax of goto labels and the restricted conditions of the goto's use in Ada makes this statement not as harmful as might be thought, and in many cases it is preferable and more legible and meaningful than some equivalent constructs (a fake goto built with an exception, for instance).

Coding Hints

When manipulating arrays, do not assume that their index starts at 1. Use the attributes 'Last, 'First, 'Range.

Define the most common constrained subtype of your unconstrained types-records mostly-and use those subtypes for parameters and return values to increase self-checking in the client code:

type Style is (Regular, Bold, Italic, Condensed);
type Font (Variety: Style) is ...
subtype Regular_Font is Font (Variety => Regular);
subtype Bold_Font is Font (Variety => Bold);
function Plain_Version (Of_The_Font: Font) return Regular_Font;
procedure Oblique (The_Text   : in out Text;
                   Using_Font : in     Italic_Font);
...    

Chapter 8

Visibility Issues

Overloading and Homographs

The following guidelines are recommended:

Overload subprograms.

Do make sure, however, when using the same identifier, that it is really implying the same kind of operation.

Avoid the hiding of homograph identifiers in nested scopes.

This leads to confusion for the reader and potential risks in maintenance. Be aware also of the existence and scope of "for" loop control variables.

Do not overload operations on subtypes, always on the type.

Contrary to what the naive reader may be led to believe, the overloading will apply to the base type and all its subtypes.

Example:

subtype Table_Page is Syst.Natural16 range 0..10;
function "+"(Left, Right: Table_Page) return Table_Page;    

The compiler looks for the base type and not the subtype of a parameter when matching subprograms. Therefore, in the above example, "+" is actually redefined for all Natural16 values in the current package, not just Table_Page. Thus any expression "Natural16 + Natural16" would now be mapped to a call to "+"(Table_Page, Table_Page), which would probably return the wrong result or produce an exception.

Context Clauses

Minimize the number of dependencies introduced by "with" clauses.

Where visibility is extended by the use of a "with" clause, the clause should cover as small a region of code as possible. Use a "with" clause only when necessary, ideally only on a body, or even on a large body stub.

Use interface packages to re-export low-level entities, thus avoiding visibly "with"-ing a large number of low-level packages. To do so, use derived types, renaming, skin subprograms, and, perhaps, predefined types such as strings (as is done with Environment command packages).

Use soft (weak) coupling between units by using generic formal parameters, rather than hard (strong) coupling by using "with" clauses.

Example: To export a Put procedure on a composite type, import as generic formals some procedure Put for its components, instead of directly withing Text_Io.

"Use" clauses should not be used.

Avoiding "use" clauses as much as possible increases readability and legibility, provided this rule is adequately supported by naming conventions that make effective use of the context and by appropriate renaming. (See "Naming Conventions," above). It also helps prevent some visibility surprises, especially during the maintenance phase.

For a package defining a character type, a "use" clause is necessary in any compilation unit that needs to define string literals based on this character type:

package Internationalization is
    type Latin_1_Char is (..., 'A', 'B', 'C', ..., U_Umlaut, ...);
    type Latin_1_String is array (Positive range <>) of 
            Latin_1_Char;
end Internationalization ;
use Internationalization;
Hello : constant Latin_1_String := "Baba"    

The absence of a "use" clause prevents the use of operators in infix form. Those can be renamed in the client unit:

function "=" (X, Y : Subscriber.Id) return Boolean 
            renames Subscriber."=";
function "+" (X, Y :Base_Types.Angle) return Base_Types.Angle
            renames Base_Types."+";    

Since the absence of a "use" clause often leads to including the same set of renamings in numerous client units, all those renamings can be factorized in the defining package itself, by means of a package Operations nested in the defining package. A "use" clause on package Operations is then recommended in the client unit:

package Pack is
    type Foo is range 1 .. 10;
    type Bar is private;
     ...
    package Operations is
        function "+" (X, Y : Pack.Foo) return Pack.Foo 
                renames Pack."+";
        function "=" (X, Y : Pack.Foo) return Boolean 
                renames Pack."=";
        function "=" (X, Y : Pack.Bar) return Boolean 
                renames Pack."=";
        ...
    end Operations;
private
	...
end Pack;
with Pack;
package body Client is
    use Pack.Operations; -- Makes ONLY Operations directly visible.
    ...
    A, B : Pack.Foo;    -- Still need prefix Pack.
    ...
    A := A + B ;        -- Note that "+" is directly 
                        -- visible.    

Package Operations should always have this name and should always be placed at the bottom of the visible part of the defining package. The "use" clause should be placed only where necessary-that is, it should be placed only in the body of Client if no operation is used in the specification, which is often the case.

  • A "use" clause can be tolerated for global packages defining scalar types, such as package Baty_System_Types or Baty_Physical_Unit_Types, or for some widely used or standard mathematical packages.
  • A "use" clause can be tolerated to get rid of highly repetitive prefixing over a short span of code. For instance, the definition of a large aggregate, based on some enumeration type defined in another package, will be easier to read without the systematic prefix on the enumeration literals. When such a "use" clause is used, it should be placed so as to minimize its scope. One way to achieve this is to have a nested package specification or declare block:
with Defs;
package Client is
    ...
    package Inner is
        use Defs;
        ...
    end Inner;		-- The scope of the use clause ends here.
    ...
end Client;
declare
    use Special_Utilities;
begin
    ...
end;                -- The scope of the use clause ends here.    

Renamings

Use renaming declarations.

Renaming is recommended in conjunction with the restriction on "use" clauses to make the code easier to read. When a unit with a very long name is referred to several times, providing a very short name for it will enhance legibility:

with Directory_Tools;
with String_Utilities;
with Text_Io;
package Example is
    package Dt renames Directory_Tools;
    package Su renames String_Utilities;
    package Tio renames Text_Io;
    package Dtn renames Directory_Tools.Naming;
    package Dto renames Directory_Tools.Object;
        ...    

The choice of short names should be consistent throughout the project, in keeping with the minimal-surprise principle. The way to achieve this is to provide the short name in the package itself:

package With_A_Very_Long_Name is package Vln renames 
            With_A_Very_Long_Name;
    ...
end
with With_A_Very_Long_Name;
package Example is package Vln renames With_A_Very_Long_Name;
-- From here on Vln is an abbreviation.    

Be aware that a package renaming gives visibility only to the visible part of the renamed package.

Imported package renamings must be grouped at the beginning of the declarative part and alphabetically sorted.

Renaming can be used locally wherever it will enhance legibility (there is no runtime penalty for doing so). Types can be renamed as subtypes without restriction.

As shown in the section on comments, renaming often provides an elegant and maintainable way to document the code-for example, to give a simple name to some complex object or to refine locally the meaning of a type. The scope of the renaming identifier should be chosen to avoid introducing confusion.

Renaming exceptions allows exceptions to be factorized among several units-for example, among all instantiations of a generic package. Note that, in a package deriving a type, exceptions potentially raised by the derived subprograms should be reexported together with the derived type to avoid the clients having to "with" the original package:

PRE>with Inner_Defs; package Exporter is ... procedure May_Raise_Exception; -- Raises exception Inner_Defs.Bad_Schmoldu when ... ... Bad_Schmoldu : exception renames Inner_Defs.Bad_Schmoldu; ...

Renaming subprograms with different default values for "in" parameters may allow simple code factorization and enhance legibility:

procedure Alert (Message : String;
                 Beeps   : Natural);
procedure Bip (Message : String := "";
               Beeps   : Natural := 1) 
        renames Alert;
procedure Bip_Bip (Message : String := "";
                   Beeps   : Natural := 2) 
        renames Alert;
procedure Message (Message : String;
                   Beeps   : Natural := 0)
        renames Alert;
procedure Warning (Message : String;
                   Beeps   : Natural := 1)
        renames Alert;    

Avoid using the name of the renamed entity (the old name) within the immediate scope of the renaming declaration; use only the identifier or operator symbol introduced by the renaming declaration (the new name).

Note about Use Clauses

For many years there has been a "use" clause controversy in the Ada community, verging sometimes on a religious war. Both parties have used various arguments that often do not scale well to large projects or examples that are far too unrealistic-or deliberately unfair.

Advocates of the "use" clause claim that it increases legibility, and they provide examples of especially unreadable, long, and redundant names, which would benefit from being renamed if used several times. They also claim that an Ada compiler can resolve overloading, which is true, but a human being immersed in a large Ada program cannot do overloading resolution as reliably as a compiler, and certainly not as fast. They claim that sophisticated APSEs, such as the Rational Environment, make the explicit fully qualified names useless; but this is not true-the user should not have to press [Definition] for each identifier he or she is not sure of. The user should not have to guess, but should be able to see immediately which objects and which abstractions are used. Rosen advocates of the "use" clause deny its potential dangers in program maintenance and suggest giving an F grade to the programmer who creates such risks; we think that fully qualified names eliminate that risk.

If the methods suggested above to alleviate the impact of the restriction on "use" clauses seem to require too much typing, consider the conclusion of Norman H. Cohen: "Any time saved when a program is being typed will be lost many times over when the program is reviewed, debugged, and maintained."

Finally, it has been shown that in large systems the absence of "use" clauses improves compilation time by reducing lookup overhead in symbol tables.

The reader interested in learning more about the use clause controversy can consult the following sources:

D. Bryan, "Dear Ada," Ada Letters, 7, 1, January-February 1987, pp. 25-28.

J. P. Rosen, "In Defense of the Use Clause," Ada Letters, 7, 7, November-December 1987, pp. 77-81.

G. O. Mendal, "Three Reasons to Avoid the Use Clause," Ada Letters, 8, 1, January-February 1988, pp. 52-57.

R. Racine, "Why the Use Clause Is Beneficial," Ada Letters, 8, 3, May-June 1988, pp. 123-127.

N. H. Cohen, Ada as a Second Language, McGraw-Hill (1986), pp. 361-362.

M. Gauthier, Ada-Un Apprentissage, Dunod-Informatique, Paris (1989), pp. 368-370.]


Chapter 9

Program Structure and Compilation Issues

Decomposition of Packages

There are two fundamental ways to decompose a large "logical" package, resulting from an initial design phase into several smaller Ada library units that are easier to manage, compile, maintain, and understand:

a) The nested decomposition

This approach emphasizes the use of Ada subunits and/or subpackages. The major subprograms, task bodies, and inner package bodies are systematically separated. The process is recursively repeated within those subunits/subpackages.

b) The flat decomposition

The logical package is decomposed into a network of smaller packages that are interconnected by "with" clauses, and the original logical package is mostly a re-exporting skin (or a design artifact that no longer even exists).

Each approach has its advantages and disadvantages. The nested decomposition requires less code to be written and leads to simpler naming (many identifiers do not need prefixing); and, on the Rational Environment at least, the structure is very visible in the library image and the structure is easier to transform (commands Ada.Make_Separate, Ada.Make_Inline). The flat decomposition often leads to less recompilation and better or cleaner structure (particularly at subsystem boundaries); it also fosters reuse. It is also easier to manage with automatic recompilation tools and configuration management. However, with the flat structure, there is a greater risk of violating the original design by "with"-ing some of the lower-level packages that have been created in the decomposition.

The level of nesting should be limited to three for subprograms, and to two for packages; do not nest packages within subprograms.

package Level_1 is
    package Level_2 is
package body Level_1 is
    procedure Level_2 is
        procedure Level_3 is    

Use body stubs for nested units ("separate bodies") when:

the body is large (more than a page of printed text) or,

the body has dependencies on other units that the rest of the package body does not, or

multiple variant versions of the body exist (e.g., for the support of different hardware or operating system).

Structure of Declarative Parts

Package Specification

The declarative part of a package specification contains declarations that should be arranged in the following sequence:

1) Renaming declaration for the package itself

2) Renaming declarations for imported entities

  • first imported packages (in alphabetical order)
  • then other entities: subprograms, types, exceptions.

3) "Use" clauses

4) Named numbers

5) Type and subtype declarations

6) Constants

7) Exception declarations

8) Exported subprogram specifications

9) Nested packages, if any

10) Private part.

For a package that introduces several major types, it may be better to have several sets of related declarations:

5) Type and subtype declarations for A

6) Constants

7) Exception declarations

8) Exported subprogram specifications for operations on A

5) Type and subtype declarations for B

6) Constants

7) Exception declarations

8) Exported subprogram specifications for operations on B

Etc.

When the declarative part is large (>100 lines) use small comment blocks to delimit the various sections.

Package Body

The declarative part of a package body declarations contains declarations that should be arranged in the following sequence:

1) Renaming declarations (for imported entities)

2) "Use" clauses

3) Named numbers

4) Type and subtype declarations

5) Constants

6) Exception declarations

7) Local subprogram specifications

8) Local subprogram bodies

9) Exported subprogram bodies

10) Nested package bodies, if any.

Other Constructs

Other declarative parts, such as in subprogram bodies, task bodies and block statements follow the same general pattern.

Context Clauses

Use one "with" clause per imported library unit. Sort the with clauses in alphabetical order. If a "use" clause on a "with"-ed unit is appropriate, then it should immediately follow the corresponding "with" clause. See below for the pragma Elaborate.

Elaboration Order

Do not rely on the order of elaboration of library units to achieve any specific effect.

Each Ada implementation is free to choose a strategy to compute the elaboration order, provided it satisfies the very simple rules stated in the Ada Reference Manual [ISO87]. Some implementations use smarter strategies than others (such as elaborating the bodies as soon as feasible after the corresponding spec), and some implementations do not bother to be this smart (especially for generic instantiations), leading to very severe portability problems.

There are three main sources for the infamous "access before elaboration" error during program elaboration (which should normally raise the Program_Error exception):

  • Attempting to instantiate a generic unit before its body has been elaborated.
  • Attempting to call a subprogram before its body has been elaborated. This is likely to occur when the elaboration of objects calls a function-for instance, to return a constraint or an initial value. This may not be highly visible if the object is a record whose (sub)components have default initial values obtained by function calls.
  • Attempting to activate a task before its body has been elaborated. This will occur, for instance, when there is a task object allocation between the task type specification and the task body elaboration:
task type T;
type T_Ptr is access T;
SomeT : T_Ptr := new T; -- Access before elaboration.    

To avoid problems in porting applications from one Ada compiler to another, the programmer should either eliminate the problems by restructuring the code (which is not always possible) or explicitly take control of elaboration order by means of pragma Elaborate, using the following strategy:

In the context clause of a unit Q, a pragma Elaborate should be applied to each unit P that appears in a "with" clause:

  • If P is or contains a generic unit that is instantiated in Q
  • If P exports a task type that is used to elaborate an object in Q.

Moreover, if P exports a type T such that the elaboration of objects of type T calls a function in package R, then the context clause of Q should contain:

with R;
pragma Elaborate (R);    

even if there are no direct references to R in Q!

Practically, it may be easier (but not always possible) to state the rule that package P should include:

PRE>with R; pragma Elaborate (R);

and the package Q must simply carry:

with P;
pragma Elaborate (P);    

P>therefore providing the right elaboration order by transitivity.


Chapter 10

Concurrency

Restrict the use of tasks.

Tasks are a very powerful feature, but they are delicate to use. Large overhead in space and time may be associated with the injudicious use of tasks. Small changes to some part of the system may completely jeopardize the liveness of a set of tasks, leading to starvation and/or deadlocks. Testing and debugging tasking programs is difficult. Therefore the use of tasks, their placement, and their interaction is a project-level decision. Tasks cannot be used in a hidden way or written by inexperienced programmers. The tasking model of an Ada program needs to be made visible and understandable.

Unless there is effective support from parallel hardware, tasks should be introduced only when concurrency is truly necessary. This is the case when expressing actions that depend on time: periodic activities or introduction of time-outs, or actions that depend on an external event such as an interrupt or the arrival of an external message. Tasks also need to be introduced to decouple other activities, such as: buffering, queuing, dispatching, and synchronizing access to common resources.

Specify the task stack size with a 'Storage_Size length clause.

For the same reasons and in the same circumstances that led to the requirement that collections have length clauses ("Access Types" section, above), the size of a task should be specified in cases where memory is a precious resource. To do so, always declare tasks of an explicitly declared type (since the length clause can be applied only to a type). A function call maybe used to dynamically size the stack.

Note: It may be very difficult to guess how much stack each task requires. To facilitate this, the runtime system can be instrumented with a "high-water mark" mechanism.

Use an exception handler in the body of a task to avoid or at least report the unexplained death of a task.

Tasks that do not handle exceptions die-usually silently. If at all feasible, try to report the nature of the death, especially Storage_Error. This will allow fine-tuning the stack size. Note that this requires allocation (primitive new) to be encapsulated in a subprogram that reexports an exception other than Storage_Error.

Create tasks during program elaboration.

For the same reasons and in the same circumstances that led to the requirement that collections be allocated during program elaboration ("Access Types" section, above), the whole application tasking structure should be created very early at program startup. It is better to have the program not start at all because of memory exhaustion than to die a couple of days later.

In subsequent rules, a distinction is made between service tasks and application tasks. Service tasks are small and algorithmically simple tasks that are used to provide the "glue" between application-related tasks. Examples of service tasks (or intermediary tasks) include buffers, transporters, relays, agents, monitors, and so on that usually provide synchronization, decoupling, buffering, and waiting services. Application tasks, as the name conveys, are more directly related to the primary functions of the application.

Avoid hybrid tasks: application tasks should be made pure callers; service tasks should be made pure callees.

A pure callee is a task that contains only accept statements or selective waits and no entry calls.

Avoid circularities in the graph of entry calls.

This will considerably reduce the risk of deadlocks. Avoid circularities at least in the system's steady-state, if they cannot be avoided completely. These two rules also make the structure easier to understand.

Restrict the use of shared variables.

Be particularly aware of hidden shared variables-that is, variables that are hidden in package bodies, for instance, and accessed by primitives visible to several tasks. Shared variables can be used in extreme cases for synchronization of access to common data structures, when the cost of rendezvous is too high. Check whether pragma Shared is effectively supported.

Restrict the use of abort statements.

The abort statement is universally recognized as one of the most dangerous and harmful primitives of the language. Its usage to terminate tasks unconditionally (and almost asynchronously) makes it almost impossible to reason about the behavior of a given tasking structure. However, there are very limited circumstances in which an abort statement is necessary.

Example: Some low-level services are provided that have no facility for time-out. The only way to introduce a time-out is to have the service provided by some auxiliary agent task, to wait (with a time-out) for a reply from the agent, and then to kill the agent with an abort if the service has not been provided within the delay time.

An abort is tolerable when it can be demonstrated that only the aborter and the abortee can be affected-for example, when no other task can possibly call the aborted task.

Restrict the use of delay statements.

Arbitrary suspension of a task may lead to severe scheduling problems, which are hard to track down and correct.

Restrict the use of attributes 'Count, 'Terminated, and 'Callable.

Attribute 'Count should be used only as a rough indication, and scheduling decisions should not be based on its value being zero or not, since the actual number of waiting tasks can change between the time the attribute is evaluated and the time its value is used.

Use conditional entry calls (or the equivalent construct with accept) to reliably check the absence of waiting tasks.

select
    The_Task.Some_Entry;
else
    -- do something else
end select;    

rather than:

if The_Task.Some_Entry'Count > 0 then
    The_Task.Some_Entry;
else
    -- do something else
end if;    

Attribute 'Terminated is meaningful only when it yields True and 'Callable when it yields False, thereby considerably limiting their usefulness. They should not be used to provide synchronization between tasks during system shutdown.

Restrict the use of priorities.

Priorities in Ada have a limited impact on scheduling. In particular, priorities of tasks waiting on entries are not taken into account for ordering the entry queues or for selecting the entry to serve in a selective wait. This may lead to priority inversion (see [GOO88]) Priorities are used by the scheduler only to select the next task to run among the tasks ready to run. Because of the risk of priority inversion, do not rely on priorities for mutual exclusion.

By using families of entries, it is possible to split the entry queue into several subqueues, and with this it is often possible to introduce an explicit concept of urgency.

If priorities are not necessary, do not assign any priority to any task.

Once a priority is assigned to one task, assign a priority to all tasks in the application.

This rule is necessary because the priorities of tasks without a pragma Priority are undefined.

For portability, keep the number of priority levels small.

The range of the subtype System.Priority is implementation-defined, and experience shows that the actual range available varies enormously from system to system. Moreover, it is a good idea to centrally define the priorities, giving them names and definitions, rather than using integer literals in all tasks. Having such a central System_Priorities package eases portability and, together with the previous rule, allows easy location of all task specifications.

To avoid drift in cyclic tasks, program the delay statement to take into account processing time, overhead, and task preemption:

Next_Time := Calendar.Clock;
loop
    -- Do the job.
    Next_Time := Next_Time + Period;
    delay Next_Time - Clock;
end loop;    

Note that Next_Time - Clock may be negative, indicating that the cyclic task is running late. It may be possible to drop one cycle.

To guarantee schedulability, assign priorities to cyclic tasks according to the Rate Monotonic Scheduling Algorithm-that is, the highest priority to the most frequent task. (See [SHA90] for more details.)

Assign a higher priority to very fast intermediary servers: monitors, buffers.

But then make sure that these servers do not block themselves by rendezvousing with other tasks. Document this priority in the code so that it can be respected during program maintenance.

To minimize the effect of "jitter," rely on time-stamping input samples or output data, rather than on the period itself.

Avoid busy wait (polling).

Make sure tasks wait with select or entry calls, or are delayed, rather than furiously checking for something to do.

For each rendezvous, make sure that at least one side is waiting and that only one side has a conditional entry call or timed entry call or waits.

Otherwise, notably in loops, there is the risk of the code running into a race condition, highly similar in result to a busy wait. This may be aggravated by poor use of priorities.

When encapsulating tasks, be sure to leave some of their special characteristics highly visible.

If entry calls are hidden in subprograms, make sure the reader of the specification of those subprograms is aware that the call to this subprogram may block. Additionally, specify whether the wait is bounded; if so, provide some estimate of the upper bound. Use a naming convention to indicate the potential wait ("Subprograms" section, above).

If the elaboration of a package, the call of a subprogram, or the instantiation of a generic unit activates a task, make this fact visible to the client:

package Mailbox_Io is
    -- This package elaborates an internal Control task
    -- that synchronizes all access to the external 
    -- mailbox 
    procedure Read_Or_Wait
        (Name: Mailbox.Name; Mbox: in out Mailbox.Object);
        --
        -- Blocking (unbounded wait).    

Do not rely on any specific order for entry selection in a selective wait.

If some fairness is required in picking up tasks queued in entries, achieve this by explicitly checking the queues with no wait in the desired order and then wait on all entries. Do not use 'Count.

Do not rely on any specific activation order for tasks elaborated in the same declarative part.

If a specific startup ordering is sought, this should be achieved by making rendezvous with special startup entries.

Implement tasks to terminate normally.

Unless the nature of the application requires that tasks, once activated, run forever, tasks should terminate, either by reaching normal completion or through a terminate alternative. This may be impossible to achieve for tasks whose master is a library-level package, since the Ada Reference Manual does not specify under which condition they should terminate.

If the master-dependent structure does not allow clean termination, then tasks should provide and wait for special shutdown entries, which are called during system shutdown.


Chapter 11

Error Handling and Exceptions

The general philosophy is to use exceptions only for errors: logic and programming errors, configuration errors, corrupted data, resource exhaustion, etc. The general rule is that the systems in normal condition and in the absence of overload or hardware failure should not raise any exceptions.

Use exceptions to handle logic and programming errors, configuration errors, corrupted data, resource exhaustion. Report exceptions by the appropriate logging mechanism as early as possible, including at the point of raise.

Minimize the number of exceptions exported from a given abstraction.

In large systems, having to handle a large number of exceptions at each level makes the code difficult to read and to maintain. Sometimes the exception processing dwarfs the normal processing.

There are several ways to minimize the number of exceptions:

  • Export only a few exceptions but provide "diagnosis" primitives that allow querying the faulty abstraction or the bad object for more detailed information about the nature of the problem that occurred.
  • Share exceptions between generic instantiations by defining the exceptions in an auxiliary nongeneric package and renaming them in the generic package for convenience.
  • Import, as generic formal procedures, the actions to be performed in the case of errors, rather than raising exceptions.
  • Add "exceptional" states to the objects, and provide primitives to check explicitly the validity of the objects.

Do not propagate exceptions not specified in the design.

Avoid a when others alternative in exception handlers, unless the caught exception is reraised.

This allows some local housekeeping without interfering with exceptions that cannot be handled at this level:

exception
    when others => 
        if Io.Is_Open (Local_File) then
            Io.Close (Local_File);
        end if;
        raise;
end;    

Another place where a when others alternative may be used is at the bottom of a task body.

Do not use exceptions for frequent, anticipated events.

There are several inconveniences in using exceptions to represent conditions that are not clearly errors:

  • It is confusing.
  • It usually forces some disruption in the flow of control that is more difficult to understand and to maintain.
  • It makes the code more painful to debug, since most source-level debuggers flag all exceptions by default.

For instance, do not use an exception as some form of extra value returned by a function (like Value_Not_Found in a search); use a procedure with an "out" parameter, or introduce a special value meaning Not_Found, or pack the returned type in a record with a discriminant Not_Found.

Do not use exceptions to implement control structures.

This is a special case of the previous rule: exceptions should not be used as a form of "goto" statement.

When catching predefined exceptions, place the handler in a very small frame surrounding the construct raising it.

Predefined exceptions like Constraint_Error, Storage_Error, and so on can occur in many places. If one such exception needs to be caught for some specific reason, the handler must be as limited in scope as possible:

begin
    Ptr := new Subscriber.Object;
exception
    when Storage_Error => 
        raise Subscriber.Collection_Overflow;
end;    

Terminate exception handlers in functions with either a "return" statement or a "raise" statement. Otherwise the Program_Error exception will be raised in the caller.

Restrict the suppressing of checks.

With today's Ada compilers, the potential reductions in code size and increases in performance obtained by suppressing checks have become marginal. Therefore, suppressing checks should be restricted to very limited pieces of code that have been identified (by doing measurements) as performance bottlenecks; it should never be applied widely to a whole system.

As a corollary, do not add extra explicit range and discriminant checking just for the improbable case that someone will decide later to suppress checks. Rely on Ad's built-in constraint-checking facilities.

Do not propagate exceptions out of the scope of their declaration.

This will make it impossible for client code to explicitly handle the exception, other than with a when others alternative, which may not be specific enough.

A corollary to this rule is: when re-exporting a type by derivation, think of re-exporting the exceptions that the derived subprograms may raise-by renaming, for instance. Otherwise, the clients will have to "with" the original defining package.

Always handle Numeric_Error and Constraint_Error together.

The Ada Board has decided that all circumstances that would have raised Numeric_Error should raise Constraint_Error instead.

Make sure status codes have an appropriate value.

When using status code returned by subprograms as an "out" parameter, always make sure a value is assigned to the "out" parameter by making this the first executable statement in the subprogram body. Systematically make all statuses a success by default or a failure by default. Think of all possible exits from the subprogram, including exception handlers.

Perform safety checks locally; do not expect your client to do so.

That is, if a subprogram might produce erroneous output unless given proper input, install code in the subprogram to detect and report invalid input in a controlled manner. Do not rely on a comment that tells the client to pass proper values. It is virtually guaranteed that sooner or later that comment will be ignored, resulting in hard-to-debug errors if the invalid parameters are not detected.

For further information, see [KR90b].


Chapter 12

Low-Level Programming

This section deals with Ada features that are a priori non-portable. They are defined in chapter 13 of the Reference Manual for the Ada Programming Language [ISO87], and the compiler-specific features are described in the "Appendix F" provided by the Ada compiler vendors.

Representation Clauses and Attributes

Study carefully Appendix F of the Ada Reference Manual (and conduct small experiments to ensure that it is well understood).

Restrict the use of representation clauses.

Representation clauses are not supported uniformly from implementation to implementation. Their use contains many traps. Therefore, they should not be used freely in a system.

Representation clauses may be necessary:

  • to interface with some specific hardware (peripheral chips, instrumentation devices, and so on) or external software (operating system)
  • to guarantee interoperability with other software: freezing the representation avoids running into problems when using different Ada compilers or just different versions of the same compiler
  • in some limited cases, to provide space optimization (memory, disk, transmission)
  • to defeat strong typing (in conjunction with unchecked conversions)
  • to constrain the size of task types and collections on systems with limited memory
  • to force 'Small equal to 'Delta for fixed-point types.

Representation clauses can be avoided in the following kinds of situations:

  • when an enumeration clause is used to "jump" over a very few missing values, the values might be introduced explicitly, with a name conveying clearly the fact that those values do not exist

Example:

Replace:

type Foo is (Bla, Bli, Blu, Blo);
for Foo use (Bla => 1, Bli =>3, Blu => 4, Blo => 5);    

with:

type Foo is (Invalid_0, Bla, Invalid_2, Bli, Blu, Blo);    
  • when the intent of a record representation clause is to have a more compact storage, it may be sufficient to apply a length clause (or a pragma Pack) to each component and subcomponent, and then apply a pragma Pack to the record type.

Group types that have representation clauses into packages clearly identified as containing implementation-dependent code.

Never assume a specific order in record layout.

In a record representation clause, always specify the placement of all discriminants, and do so before specifying any components in the variants.

Avoid alignment clauses.

Trust the compiler to do a good job; it knows the target alignment constraints. programmer's use of alignment clauses is likely to lead to alignment conflicts later.

Be aware of the existence of compiler-generated fields in unconstrained composite types:

in records: offset of dynamic fields, variant clause index, constrained bit, and so on

in arrays: dope vectors.

Refer to the Appendix F for the compiler for details. Do not rely on what is written in chapter 13 of the Ada Reference Manual [ISO87].

Unchecked Conversions

Restrict the use of Unchecked_Conversion.

The extent of support for Unchecked_Conversion varies greatly from one Ada compiler to another, and its precise behavior may be slightly different, especially when applied to composite types and access types.

In an instantiation of Unchecked_Conversion, ensure that both source and target types are constrained and have the same size.

This is the only way to achieve some limited portability and to avoid running into problems with implementation-added information such as dope vectors. One way to make sure both types have the same size is to "wrap" them in a record type with a record representation clause.

One way to make the type constrained is to do the instantiation within a "skin" function, where the constraint is computed beforehand.

Do not apply Unchecked_Conversion to access values or tasks.

Not only is this not supported by all systems (for example, the Rational native compiler), but also it should not be assumed that:

  • access values are isomorphic to a System.Address: access values may have fewer bits than machine addresses .address;
  • integer arithmetic on access values produces the effect that may be expected: storage may not be contiguous.

Chapter 13

Summary

We recapitulate here the most important things to watch for:

Restricted features ():

  • access types
  • fixed-point types
  • unchecked deallocation
  • "goto" statements
  • "use" clauses
  • tasks
  • shared variables
  • "abort" statements
  • "delay" statements
  • attributes 'Count, 'Callable, and 'Terminated
  • priorities
  • pragma Suppress
  • representation clauses (except 'Small)
  • Unchecked_Conversion.

Absolute "don't"s ()

  • limited types that are not self-initializing
  • uninitialized variables
  • use of predefined numeric types
  • handling of Numeric_Error separately from Constraint_Error
  • dependency on order of elaboration, evaluation, or execution (for example, subprogram parameters, aggregates, selective wait alternatives)
  • redefinition of identifiers from package Standard
  • using Ada 95 keywords or predefined identifiers
  • not using common sense.

References

This document is derived directly from Ada Guidelines: Recommendations for Designer and Programmers, Application Note #15, Rev. 1.1, Rational, Santa Clara, Ca., 1990. [KR90a]. However, many different sources have contributed to its elaboration.

BAR88 B. Bardin & Ch. Thompson, "Composable Ada Software Components and the Re-export Paradigm", Ada Letters, VIII, 1, Jan.-Feb. 1988, p.58-79.

BOO87 E. G. Booch, Software Components with Ada, Benjamin/Cummings (1987)

BOO91 Grady Booch: Object-Oriented Design with Applications, Benjamin-Cummings Pub. Co., Redwood City, California, 1991, 580p.

BRY87 D. Bryan, "Dear Ada," Ada Letters, 7, 1, January-February 1987, pp. 25-28.

COH86 N. H. Cohen, Ada as a Second Language, McGraw-Hill (1986), pp. 361-362.

EHR89 D. H. Ehrenfried, Tips for the Use of the Ada Language, Application Note #1, Rational, Santa Clara, Ca., 1987.

GAU89 M. Gauthier, Ada-Un Apprentissage, Dunod-Informatique, Paris (1989), pp. 368-370.

GOO88John B. Goodenough and Lui Sha: "The Priority Ceiling Protocol," special issue of Ada Letters, Vol., Fall 1988, pp. 20-31.

HIR92 M. Hirasuna, "Using Inheritance and Polymorphism with Ada in Government Sponsored Contracts", Ada Letters, XII, 2, March/April 1992, p.43-56.

ISO87 Reference Manual for the Ada Programming Language, International Standard ISO 8652:1987.

KR90a Ph. Kruchten, Ada Guidelines: Recommendations for Designer and Programmers, Application Note #15, Rev. 1.1, Rational, Santa Clara, Ca., 1990.

KR90b Ph. Kruchten, "Error-Handling in Large, Object-Based Ada Systems," Ada Letters, Vol. X, No. 7, (Sept. 1990), pp. 91-103.

MCO93 Steve McConnell, Code Complete-A Practical Handbook of Software Construction, Microsoft®Press, Redmond, WA, 1993, 857p.

MEN88 G. O. Mendal, "Three Reasons to Avoid the Use Clause," Ada Letters, 8, 1, January-February 1988, pp. 52-57.

PER88 E. Perez, "Simulating Inheritance with Ada", Ada letters, VIII, 5, Sept.-Oct. 1988, p. 37-46.

PLO92 E. Ploedereder, "How to program in Ada 9X, Using Ada 83", Ada Letters, XII, 6, November 1992, pp. 50-58.

RAC88 R. Racine, "Why the Use Clause Is Beneficial," Ada Letters, 8, 3, May-June 1988, pp. 123-127.

RAD85 T. P. Bowen, G. B. Wigle & J. T. Tsai, Specification of Software Quality Attributes, Boeing Aerospace Company, Rome Air Development Center, Technical Report RADC-TR-85-37 (3 volumes).

ROS87 J. P. Rosen, "In Defense of the Use Clause," Ada Letters, 7, 7, November-December 1987, pp. 77-81.

SEI72 E. Seidewitz, "Object-Oriented Programming with Mixins in Ada", Ada Letters, XII, 2, March/April 1992, p.57-61.

SHA90 Lui Sha and John B. Goodenough: "Real-Time Scheduling Theory and Ada," Computer, Vol. 23, #4 (April 1990), pp. 53-62.)

SPC89 Software Productivity Consortium: Ada Quality and Style-Guidelines for the Professional Programmer, Van Nostrand Reinhold (1989)

TAY92 W. Taylor, Ada 9X Compatibility Guide, Version 0.4, Transition Technology Ltd., Cwmbrân, Gwent, U.K., Nov. 1992.

WIC89 B. Wichman: Insecurities in the Ada Programming Language, Report DITC137/89, National Physical Laboratory (UK), January 1989.


Glossary

Most terms used in this document are defined in Appendix D of the Reference Manual for the Ada Programming Language, [ISO87]. Additional terms are defined here:

ADL: Ada as a Design Language; refers to the way Ada is used to express aspects of a design; a.k.a. PDL, or Program Design Language.

Environment: The Ada software development environment in use.

Library switch: In the Rational Environment, a compilation option that applies to a whole program library.

Model world: In the Rational Environment, a special library that is used to capture uniform project-wide library switch settings.

Mutable: Property of a record whose discriminants have default values; an object of a mutable type can be assigned any value of the type, even values that make it change its discriminants, hence its structure.

Skin: A subprogram whose body acts solely as a relay. It ideally contains only one statement: a call to another subprogram, with an identical set of parameters, or parameters that convertible to and from the parameter.

PDL: Program Design Language.



Copyright  © 1987 - 2001 Rational Software Corporation


Display Rational Unified Process using frames

Rational Unified Process