5.7 Visibility
As noted in Guideline 4.2, Ada's ability to enforce information hiding
and separation of concerns through its visibility controlling features
is one of the most important advantages of the language. Subverting
these features, for example, by too liberal use of the use
clause, is
wasteful and dangerous.
The Use Clause
guideline
- When you need to provide visibility to operators, use the
use type
clause. - Avoid/minimize the use of the
use
clause (Nissen and Wallis 1984). - Consider using a package
renames
clause rather than ause
clause for a package. - Consider using the
use
clause in the following situations:- When standard packages are needed and no ambiguous references are introduced
- When references to enumeration literals are needed
- Localize the effect of all
use
clauses.
example
This is a modification of the example from Guideline 4.2.3. The effect
of a use
clause is localized:
----------------------------------------------------------------------------------
package Rational_Numbers is
type Rational is private;
function "=" (X, Y : Rational) return Boolean;
function "/" (X, Y : Integer) return Rational; -- construct a rational number
function "+" (X, Y : Rational) return Rational;
function "-" (X, Y : Rational) return Rational;
function "*" (X, Y : Rational) return Rational;
function "/" (X, Y : Rational) return Rational; -- rational division
private
...
end Rational_Numbers;
----------------------------------------------------------------------------------
package body Rational_Numbers is
procedure Reduce (R : in out Rational) is . . . end Reduce;
. . .
end Rational_Numbers;
----------------------------------------------------------------------------------
package Rational_Numbers.IO is
...
procedure Put (R : in Rational);
procedure Get (R : out Rational);
end Rational_Numbers.IO;
----------------------------------------------------------------------------------
with Rational_Numbers;
with Rational_Numbers.IO;
with Ada.Text_IO;
procedure Demo_Rationals is
package R_IO renames Rational_Numbers.IO;
use type Rational_Numbers.Rational;
use R_IO;
use Ada.Text_IO;
X : Rational_Numbers.Rational;
Y : Rational_Numbers.Rational;
begin -- Demo_Rationals
Put ("Please input two rational numbers: ");
Get (X);
Skip_Line;
Get (Y);
Skip_Line;
Put ("X / Y = ");
Put (X / Y);
New_Line;
Put ("X * Y = ");
Put (X * Y);
New_Line;
Put ("X + Y = ");
Put (X + Y);
New_Line;
Put ("X - Y = ");
Put (X - Y);
New_Line;
end Demo_Rationals;
rationale
These guidelines allow you to maintain a careful balance between
maintainability and readability. Use of the use
clause may indeed make
the code read more like prose text. However, the maintainer may also
need to resolve references and identify ambiguous operations. In the
absence of tools to resolve these references and identify the impact of
changing use clauses, fully qualified names are the best alternative.
Avoiding the use
clause forces you to use fully qualified names. In
large systems, there may be many library units named in with
clauses.
When corresponding use
clauses accompany the with
clauses and the
simple names of the library packages are omitted (as is allowed by the
use
clause), references to external entities are obscured and
identification of external dependencies becomes difficult.
In some situations, the benefits of the use
clause are clear. A
standard package can be used with the obvious assumption that the reader
is very familiar with those packages and that additional overloading
will not be introduced.
The use type
clause makes both infix and prefix operators visible
without the need for renames
clauses. You enhance readability with the
use type
clause because you can write statements using the more
natural infix notation for operators. See also Guideline 5.7.2.
You can minimize the scope of the use
clause by placing it in the body
of a package or subprogram or by encapsulating it in a block to restrict
visibility.
notes
Avoiding the use
clause completely can cause problems with enumeration
literals, which must then be fully qualified. This problem can be solved
by declaring constants with the enumeration literals as their values,
except that such constants cannot be overloaded like enumeration
literals.
An argument defending the use clause can be found in Rosen (1987).
automation notes
There are tools that can analyze your Ada source code, resolve
overloading of names, and automatically convert between the use
clause
or fully qualified names.
The Renames Clause
guideline
- Limit the scope of a renaming declaration to the minimum necessary scope.
- Rename a long, fully qualified name to reduce the complexity if it becomes unwieldy (see Guideline 3.1.4).
- Use renaming to provide the body of a subprogram if this subprogram merely calls the first subprogram.
- Rename declarations for visibility purposes rather than using the use clause, except for operators (see Guideline 5.7.1).
- Rename parts when your code interfaces to reusable components originally written with nondescriptive or inapplicable nomenclature.
- Use a project-wide standard list of abbreviations to rename common packages.
- Provide a
use type
rather than arenames
clause to provide visibility to operators.
example
procedure Disk_Write (Track_Name : in Track;
Item : in Data) renames
System_Specific.Device_Drivers.Disk_Head_Scheduler.Transmit;
See also the example in Guideline 5.7.1, where a package-level renames
clause provides an abbreviation for the package Rational_Numbers_IO
.
rationale
If the renaming facility is abused, the code can be difficult to read. A
renames
clause can substitute an abbreviation for a qualifier or long
package name locally. This can make code more readable yet anchor the
code to the full name. You can use the renames
clause to evaluate a
complex name once or to provide a new "view" of an object (regardless
of whether it is tagged). However, the use of renames
clauses can
often be avoided or made obviously undesirable by carefully choosing
names so that fully qualified names read well.
When a subprogram body calls another subprogram without adding local data or other algorithmic content, it is more readable to have this subprogram body rename the subprogram that actually does the work. Thus, you avoid having to write code to "pass through" a subprogram call (Rationale 1995, §II.12).
The list of renaming declarations serves as a list of abbreviation
definitions (see Guideline 3.1.4). As an alternative, you can rename a
package at the library level to define project-wide abbreviations for
packages and then with
the renamed packages. Often the parts recalled
from a reuse library do not have names that are as general as they could
be or that match the new application's naming scheme. An interface
package exporting the renamed subprograms can map to your project's
nomenclature. See also Guideline 5.7.1.
The method described in the Ada Reference Manual (1995) for renaming a type is to use a subtype (see Guideline 3.4.1).
The use type
clause eliminates the need for renaming infix operators.
Because you no longer need to rename each operator explicitly, you avoid
errors such as renaming a +
to a -
. See also Guideline 5.7.1.
notes
You should choose package names to be minimally meaningful, recognizing
that package names will be widely used as prefixes (e.g.,
Pkg.Operation
or Object : Pkg.Type_Name;
). If you rename every
package to some abbreviation, you defeat the purpose of choosing
meaningful names, and it becomes hard to keep track of what all the
abbreviations represent.
For upward compatibility of Ada 83 programs in an Ada 95 environment, the environment includeslibrary-level renamings of the Ada 83 library level packages (Ada Reference Manual 1995, §J.1). It is not recommended that you use these renamings in Ada 95 code.
Overloaded Subprograms
guideline
Limit overloading to widely used subprograms that perform similar actions on arguments of different types (Nissen and Wallis 1984).
example
function Sin (Angles : in Matrix_Of_Radians) return Matrix;
function Sin (Angles : in Vector_Of_Radians) return Vector;
function Sin (Angle : in Radians) return Small_Real;
function Sin (Angle : in Degrees) return Small_Real;
rationale
Excessive overloading can be confusing to maintainers (Nissen and Wallis 1984, 65). There is also the danger of hiding declarations if overloading becomes habitual. Attempts to overload an operation may actually hide the original operation if the parameter profile is not distinct. From that point on, it is not clear whether invoking the new operation is what the programmer intended or whether the programmer intended to invoke the hidden operation and accidentally hid it.
notes
This guideline does not prohibit subprograms with identical names declared in different packages.
Overloaded Operators
guideline
- Preserve the conventional meaning of overloaded operators (Nissen and Wallis 1984).
- Use "
+
" to identify adding, joining, increasing, and enhancing kinds of functions. - Use "
-
" to identify subtraction, separation, decreasing, and depleting kinds of functions. - Use operator overloading sparingly and uniformly when applied to tagged types.
example
function "+" (X : in Matrix;
Y : in Matrix)
return Matrix;
...
Sum := A + B;
rationale
Subverting the conventional interpretation of operators leads to confusing code.
The advantage of operator overloading is that the code can become more clear and written more compactly (and readably) when it is used. This can make the semantics simple and natural. However, it can be easy to misunderstand the meaning of an overloaded operator, especially when applied to descendants. This is especially true if the programmer has not applied natural semantics. Thus, do not use overloading if it cannot be used uniformly and if it is easily misunderstood.
notes
There are potential problems with any overloading. For example, if there
are several versions of the "+"
operator and a change to one of them
affects the number or order of its parameters, locating the occurrences
that must be changed can be difficult.
Overloading the Equality Operator
guideline
- Define an appropriate equality operator for private types.
- Consider redefining the equality operator for a private type.
- When overloading the equality operator for types, maintain the properties of an algebraic equivalence relation.
rationale
The predefined equality operation provided with private types depends on the data structure chosen to implement that type . If access types are used, then equality will mean the operands have the same pointer value. If discrete types are used, then equality will mean the operands have the same value. If a floating- point type is used, then equality is based on Ada model intervals (see Guideline 7.2.7). You should, therefore, redefine equality to provide the meaning expected by the client. If you implement a private type using an access type, you should redefine equality to provide a deep equality. For floating-point types, you may want to provide an equality that tests for equality within some application-dependent epsilon value.
Any assumptions about the meaning of equality for private types will create a dependency on the implementation of that type. See Gonzalez (1991) for a detailed discussion.
When the definition of "=
" is provided, there is a conventional
algebraic meaning implied by this symbol. As described in Baker (1991),
the following properties should remain true for the equality operator:
-
- Reflexive:
a = a
- Symmetric:
a = b ==> b = a
- Transitive:
a = b and b = c ==> a = c
- Reflexive:
In redefining equality, you are not required to have a result type of
Standard.Boolean
. The Rationale (1995, §6.3) gives two examples where
your result type is a user-defined type. In a three-valued logic
abstraction, you redefine equality to return one of True
, False
, or
Unknown
. In a vector processing application, you can define a
component-wise equality operator that returns a vector of Boolean
values. In both these instances, you should also redefine inequality
because it is not the Boolean complement of the equality function.