Abstract: It is shown that Ada is not suited to implementing physical dimensions with its type concept. Therefore a set of Ada packages is constructed which allow dealing with physical quantities without type conversions as far as possible while keeping the advantages of strong typing in critical cases. Basic mathematical functions like the square root and the sine are 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.
(See also SI Units - Checked and Unchecked.)
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). The considerations below will show that Ada is even absolutely unsuitable to handling dimension checking in assignments with reasonable expenditure.
The conceptual error lies in the fact that types, which describe mere number representations within some machine, are brought into connection with arbitrarily selected real world objects serving as references for measurements of physical items.
A meter is the length of the so-called Standard Meter, a platinum rod which is preserved in Paris under well-defined environmental conditions. (Once upon a time at least it was defined this way.)
A method will then be presented how to deal with physical dimensions so that mathematical functions are included, trigonometric functions observe dimension, an utmost degree of information can be included within declarations, and type conversions are not needed.
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, the relative error Delta := Delta_x/x (a pure number) has to be declared as a dimensioned variable
Delta, X, Delta_X: Length;
Delta := Delta_X/X;
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);
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 makes the compiler lose the ability to type check the assignment operation by comparing dimensions on the left and right sides, which is the very reason for introducing different types for different dimensions. Use of the second way leads to a plethora of overloaded functions. Anybody who has ever attempted to define a package of overloaded "*" and "/" operators at least for kinematics with only three dimensions length, time, and mass (let alone for the full SI system with its seven base dimensions meter, kilogramm, second, ampere, kelvin, candela, mol) including at least the fourth power in any dimension (computing the modulus of an acceleration vector leads to the fourth time power in the denominator, and you would hardly believe what kinds of unusual dimensions are met with intermediate results) knows what is meant by this aim: The number of function definitions afforded runs into the hundreds.
One could object that this definition has to be made only once and later-on the package has only to 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. It takes into account only simple multiplication and division (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). Operations like exponentiation an and root extraction root (n, a), however, are not representable at all. Here type conversions have still to be used.
So both methods of dealing with dimensions lead to drawbacks by affording type conversions. Therefore we look for a method which achieves both, minimization of type conversions and transfer of semantic information. By the above it should be clear that some sacrifices have to be made. The sacrifice we are about to make is dimension checking in assignments if only all variables belong to a coherent dimension system. This is not a big sacrifice because this check can only be done for multiplication and division with utmost expenditure and it cannot be done at all for exponentiation and root extraction. The gain on the other hand will be that the mathematical functions exist as predefined operations on the Ada base types so that only one package, the package Universe, has to be withed and used rather than several ones with the types contained in one of them and the mathematical functions in the others. The names of types and subtypes will be chosen in a way that removes any doubts about the appropriate dimension of an item in question, whereas a declaration like v: Speed; would not exhibit which dimension was the correct one [m/s, ft/min, km/h, Kts] if many different dimension systems were used concurrently in a project.
Given a generic mathematical library with all functions needed, we show the method with the two functions Sqrt and Sin serving as examples, as they include all essential features.
generic type Real is digits <>; package Generic_Mathematical_Library is PI: constant := 3.14159; function Sqrt (X: Real) return Real; function Sin (X: Real) return Real; -- Cycle 2*PI function SinD (X: Real) return Real; -- Cycle 360 degrees -- Other functions Illegal_Argument: exception; end Generic_Mathematical_Library;
This form can easily be obtained if the sine function should be given as
function Sin (X, Cycle: Real) return Real;
In a kind of creative act, base types are produced from package Generic_Mathematical_
with Generic_Mathematical_Library; -- and what else is desired -- to be derivable package Big_Bang is type Primeval_Float is digits project_defined; package Primeval_Mathematical_Library is new Generic_Mathematical_Library (Primeval_Float); -- Take subprograms out of the instantiation to make them -- derivable operations on type Primeval_Float: function Sqrt (X: Primeval_Float) return Primeval_Float renames Primeval_Mathematical_Library.Sqrt; function Sin (X: Primeval_Float) return Primeval_Float renames Primeval_Mathematical_Library.Sin; function SinD (X: Primeval_Float) return Primeval_Float renames Primeval_Mathematical_Library.SinD; -- Take out objects that are not derivable: PI: constant := Primeval_Mathematical_Library.PI; Illegal_Argument: exception renames Primeval_Mathematical_Library.Illegal_Argument; -- A type without mathematical operations: type Primeval_Float_Without_Mathematics is digits project_defined; end Big_Bang;
The precision of the base types Primeval_Float and Primeval_Float_Without_
Out of the Big Bang evolves the Universe containing all desired types, operations, exceptions, and constants. TheBig Bang itself is no longer visible, i.e. it is strictly forbidden to include this package in any others with list. Unfortunately, there is no syntax rule in Ada to force observance of this requirement. Here programmer compliance and code inspection by those responsible is asked for.
We come to the following agreement:
Within this project, all computation is done in only one coherent system of physical dimensions.
Here we choose the SI system. We define an Ada type, and for all dimensions which are frequently needed there are subtypes:
type SI_Unit is new Big_Bang.Primeval_Float; subtype Meter is SI_Unit; subtype Second is SI_Unit; subtype Meter_Per_Second is SI_Unit; subtype Newton is SI_Unit; -- kg*m/s**2 subtype Radian is SI_Unit; -- 1 subtype Without_Unit is SI_Unit; -- 1 ...
In this way, we can include semantic information within variable declarations.
g: Meter_Per_Second_2 := 9.81; t: Second := 10.0; s: Meter := 0.5 * g * t**2;
It is the programmer's own responsibility that this formula is physically correct, no type checking is done!
If all of the above subtypes were true types, the assignment would be marred with type conversions without guarantee of correctness. So where is the advantage?
s := 0.5 * Meter(g) * Meter(t)**2;
The subtype Without_Unit is thought to be used only for truely dimensionless quantities, i.e. for quantities with dimension 1:
Alpha: constant Without_Unit := e**2 / (4.0 * PI * eps_0 * h_bar * c);
Although Radian and Without_Unit physically are the same, use of the name Radian conveys the meaning of an angle.
The type SI_Unit itself is only to be used in cases where the variable possesses some dimension different from 1, but there is no subtype defined for it:
Dioptry: SI_Unit; -- 1/m
Indication of the dimension in declarations of this kind (as is done in the example) could be introduced as a requirement.
For input and output other units may be needed. All of those other units are denoted by types of their own (respectively subtypes in case of other coherent unit systems), and there are conversion functions for each of them to the corresponding SI unit.
type Kilometer is new Big_Bang.Primeval_Float_Without_Mathematics; function Kilometer_To_Meter (km: Kilometer) return Meter; function Meter_To_Kilometer ( m: Meter ) return Kilometer; type Nautical_Unit is new Big_Bang. Primeval_Float_Without_Mathematics; subtype Nautical_Mile is Nautical_Unit; subtype Hour is Nautical_Unit; subtype Knots is Nautical_Unit; -- NMi/h function Nautical_Mile_To_Meter (NMi: Nautical_Mile) return Meter; function Meter_To_Nautical_Mile (m : Meter) return Nautical_Mile; function Hour_To_Second (h: Hour ) return Second; function Second_To_Hour (s: Second) return Hour; function Knots_To_Meter_Per_Second (kts: Knots) return Meter_Per_Second; function Meter_Per_Second_To_Knots (mps: Meter_Per_Second) return Knots;
The type without mathematics was chosen deliberately because computation shall not be done in these types. In the critical case of unit mixing, strong typing thus guarantees the correct use of a coherent unit system.
Type conversions in Ada style
Kts: Knots := 10.0; v : Meter_Per_Second := Meter_Per_Second (Kts); -- Nonsense
are also strictly forbidden.
Again and unfortunately, a syntax rule to prevent this does not exist.
Let us now consider the mathematical functions. For Sqrt there is not much to say. Since the function Sqrt is known with the type SI_Unit, the statement
t := Sqrt (2.0 * s / g);
compiles. Trigonometric functions are more complicated. This will be shown by means of the sine function. The procedure for the cosine and tangent and their inverses will then be obvious. We want both of the following equations to be representable in Ada
x = x0 sin (phi) x = x0 sin (omega t)
phi being measured in degrees, omega in Hertz, i.e. the argument in the second line is dimensionless respectively has the dimension radian.
The second case is relatively simple. As in the case of Sqrt, the correct Sin function is known with the type SI_Unit so that we can write
X := X0 * Sin (Omega * T);
What is disturbing is that also the wrong function SinD which interpretes (omega t) in degrees is an operation on the type SI_Unit. There are several options to cope with this situation. Firstly one might say that the existence of this function was not disturbing at all and rather might show up useful; it is kept deliberately. Secondly, one could destroy it by hiding the derived (and therefore implicitely declared) function by a new explicitly declared homograph which when called raises an exception Illegal_Function_Call. Thirdly, the unwelcome function is hidden as well, but now by renaming it to the correct Sin function, thus making Sin and SinD identical.
For dealing with angles measured in degrees, a new type is introduced (like for the other non SI units), but this time by deriving it from the type with the mathematical functions:
type Degree is new Big_Bang.Primeval_Float;
Now there exist implicitly declared functions
function Sin (X: Degree) return Degree; function SinD (X: Degree) return Degree;
only the second of which is correct. Again there are the three options mentioned above, but now only the last one seems sensible since the information about computing in degrees is already conveyed by the argument type so that later-on it will easily be forgotten that only the SinD function leads to the correct result. If degree is defined at all as a type of its own, then Sin must consistently work in degrees. So Sin is mapped onto SinD by a renaming declaration. Consistency within the abundance of overloaded trigonometric functions created in this way requires that also in the case of an argument of type radian the last option is used. We thus arrive at the result:
The kind of trigonometric function (degree versus radian) is solely determined by the argument type.
The functions Sin and SinD always produce the same effect. Preferably there will be a requirement to only use Sin, the alias SinD is disallowed.
We are not yet at the end of the journey for the results of Sin and SinD are still Degree rather than SI_Unit. Since these functions do not disturb, they are kept; additionally a new set of functions with the desired parameter and result profile is declared:
function Sin (X: Degree) return SI_Unit; function SinD (X: Degree) return SI_Unit renames Sin;
(The second declaration is a consequence of the rule that there are both names denoting identical functions.) Now we have reached the aim of being able to compile the statement
X := X0 * SIN (Phi);
The price to pay for this achievement, namely the abundance of overloaded functions, seems adequate since all of them are uniquely discriminated by their profile
function F (X: Radian) return SI_Unit; function F (X: Degree) return SI_Unit; function F (X: Degree) return Degree ; function F_1 (X: SI_Unit) return Radian; function F_1 (X: SI_Unit) return Degree; function F_1 (X: Degree ) return Degree;
(with F_1 denoting the inverse function).
A remark concerning the direct visibility of all declarations in Universe is due. Many projects impose restrictions on use clauses or even disallow them at all, and they do so for good reasons. Since the package Universe, however, provides the very basis for everything else (as is indicated by the name), it can be looked at as residing on the same level as the predefined Ada package Standard representing the Ada "Universe". For this reason, the use clause should be allowed in this case. This could even become the requirement that Universe only be present in context clauses in the form of a with clause combined with a use clause.
See excerpts of the specification and body of package Universe.
We come to the conclusion that in spite of the seeming complexity because
of heavy function overloading these packages are proven, especially for
spheric trigonometry to convert between geographic and UTM coordinates.
They lead to the following advantages:
There is one service restriction:
This is the translation of a version that is older than the current
German version [published in
Ada Aktuell 1.1 (March 1993)] and hence not as elaborate.
Especially references to other works can be found there.
A ready to use version can be downloaded here.