Abstract
Types in Ada with their inherent strong binding lead, if applied
correctly, to great data safety, i.e. they prevent comparing inadvertently
apples with pears. This conception has proven so successful that languages
like C, which originally had only very limited typing at their disposal,
implement similar ideas in their new standards. Now also physical dimensions
have the property of being combinable only in a very special way, and every
physicist is used to checking equations with respect to dimensional correctness.
So it is tempting to impose this chore upon the compiler by representing
dimensions as types. This, however, is not that easy as can be seen by
observing that indeed meter plus meter results in meter, yet meter times
meter results in meter squared, and Ada operators work type-preserving:
For A, B of type T, the product A*B
is also of type T.
There are many examples in literature showing how to implement units
of measure correctly and safely with Ada types. They all use methods similar
to the one presented here.
Do not do this. The considerations
below will show that Ada is even absolutely unsuitable to handling
dimension checking in assignments with reasonable expenditure.
Let physical dimensions be implemented by Ada types:
type Length is new Float;
type Time is new Float;
type Speed is new Float;
The predefined multiplication and division operators are of no use since
they remain within the type, and what is more, use of types in this naive
way is liable to lead to gross misinterpretations. For error estimation e.g.,
the relative error Delta := Delta_x/x (a pure number) has to be
declared as a dimensioned variable
Delta, X, Delta_X: Length;
for
Delta := Delta_X/X; -- dimension 1
to be compilable, an awful misleading of potential readers of this program!
(It is similar nonsense to call transcendental functions like exp
for dimensioned arguments.)
There exist two possibilities to represent the equation s = vt
in Ada:
S := Length (V) * Length (T);
or
S := V * T;
the "*" operator in the latter case being defined
as follows:
function "*" (Left: Speed; Right: Time) return Length;
Use of the first way marrs the assignment with type conversions, rendering
it unreadable (at least for more complicated equations) and thus error
prone, and makes the compiler lose the ability to type check the operation
by comparing dimensions on the left and right hand sides, which is the
very reason for introducing different types for different dimensions. So
rather than gaining, we lose.
Use of the second way on the first glance seems OK. We utilize the usual
style of physical equations and the compiler checks the dimension during
compile time. So in order to avoid the problem mentioned in the first paragraph,
we use private types for our dimensioned units:
generic
type Number is digits <>;
package Dimension is
type Unit is private;
-- "=" Equality is predefined
function "<" (Left, Right: Unit) return Boolean;
function "<=" (Left, Right: Unit) return Boolean;
function ">" (Left, Right: Unit) return Boolean;
function ">=" (Left, Right: Unit) return Boolean;
function "+" (Right: Number) return Unit;
function "-" (Right: Number) return Unit;
function "+" (Right: Unit) return Unit;
function "-" (Right: Unit) return Unit;
function "abs" (Right: Unit) return Unit;
function "+" (Left, Right: Unit) return Unit;
function "-" (Left, Right: Unit) return Unit;
function "/" (Left, Right: Unit) return Number;
function "*" (Left: Number; Right: Unit ) return Unit;
function "*" (Left: Unit ; Right: Number) return Unit;
function "/" (Left: Unit ; Right: Number) return Unit;
function Measure (X: Unit) return Number;
private
type Unit is new Number;
pragma Inline ("<", "<=", ">", ">=", "+", "-", "abs", "*", "/",
Measure);
end Dimension;
The unary adding operators are used as constructors, which seems natural
enough. Note that there is no multiplication of two dimensioned units;
their devision results in a pure number.
Now we define our types:
package Physical_Item is new Dimension (Float);
type Length is new Physical_Item.Unit;
type Time is new Physical_Item.Unit;
type Speed is new Physical_Item.Unit;
function "*" (Left: Speed ; Right: Time ) return Length;
function "/" (Left: Length; Right: Time ) return Speed;
function "/" (Left: Length; Right: Speed) return Time;
Until here, everything is OK. So as long as
you stay within the limits of this simplistic model, you may well use this
method.
But you have to admit: This is not real physics.
Expanding this simple system to areas, volumes, accelerations ... affords
more types and hence more functions.
type Area is new Physical_Item.Unit;
function "*" (Left, Right: Length) return Area;
function "/" (Left: Area; Right: Length) return Length;
-- and so on for the other types
Now think of vectorial algebra. Computing the modulus of an acceleration
vector leads to the fourth time power in the denominator. We begin to suspect
this will become unruly: Until which power in each dimension shall we go?
And we are far from the end of the road: The full SI system has
seven base dimensions: meter, kilogramm, second,
ampere, kelvin, candela, mol.
Our attempt leads us to a plethora of overloaded functions. The number
of function definitions afforded runs into the hundreds. (By the
way: although dimensions of intermediate results depend on the order the
operators are executed in expressions like nRT/p, there is no
need to introduce parentheses because operators of the same precedence
level are executed in textual order from left to right).
One could object that this definition has to be made only once and for
all in a reusable package, and later-on the package can simply be withed
and used without any need for the user to care about the package's
complexity, but unfortunately the argument is not fully correct. Apart
from the most probable compile time explosion, it takes into account only
simple multiplication and division. Operations like exponentiation
an and root extraction
root (n, a) are not representable at all. So how could
we use this method for the (mixed) electrostatic unit system, which
for several reasons is far better suited to theoretical physics? One esu
(electrostatic unit) is defined as
g1/2 cm3/2 s-1, and gauss
evaluates to g1/2 cm-1/2
s-1. Here type conversions have still to be used.
So we have to confess that our attempt to let the compiler check equations
at compile time has miserably failed. The
only proper way to deal with dimensions in full generality is either to
handle them as attributes of the numeric values
that are calculated and checked at run-time or to use preprocessors.
Also for these methods, there are many examples to be found in literature.
The preprocessor method can be left aside because constructing a preprocessor
is normally too complicated.
The attribute method is unusable in cases where execution speed is essential.
So for hard real-time embedded systems, another way is needed to deal
with dimensioned items. You can find such a method on my homepage under
the title From
the Big Bang to the Universe. It has successfully been in use for many
years now in at least three avionic embedded systems. One Ada package,
the Universe, is constructed that holds all basic definitions to
allow dealing with physical quantities without type conversions while keeping
the advantages of strong typing in critical cases. Basic mathematical functions
like the square root and the sine are also included as predefined operations
of the numeric base types, thus rendering possible the dimensionally correct
computation of formulae like x=x0cos(phi) in
degrees and radians again without any type conversions. |