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
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
|