A method to deal with dimensioned items is presented that guarantees dimensional correctness during runtime. Although being runtime consuming, its application to hard real-time systems is not a priori precluded since dimensions may be switched off by changing only a few lines of code, thus reducing the computations to the pure numerics.
As every experienced Ada programmer should know, Ada is not suited to emulate physical dimensions with its static type concept, which by strong typing detects mixing of incompatible types already during compile-time. Although at first sight this seems to be suggestive, it is not so easy as can be seen by observing that meter plus meter results in meter, whereas meter times meter results in meter squared, and that Ada operators are type-preserving (for A, B of type T, A*B is also of type T) [1].
Nevertheless attempts to achieve this are continuing, and they all rely on operator overloading. One can start with a dimension algebra of the following form, which holds only those operators that are type-preserving:
generic type Real is digits <>; package Unit is type Item is private; function "+" (L, R: Item) return Item; function "-" (L, R: Item) return Item; function "/" (L, R: Item) return Real; function "*" (L: Real; R: Item) return Item; function "*" (L: Item; R: Real) return Item; function "/" (L: Item; R: Real) return Item; ... private ... end Unit;
From this package, dimensioned types are created by instantiation and derivation:
package Dimension is new Unit (Float); type Meter is new Dimension.Item; type Second is new Dimension.Item; type Meter_per_Second is new Dimension.Item;
To this, operators to mix dimensions are added:
function "/" (L: Meter; R: Second) return Meter_per_Second;
The advantage of such a method, if it were feasible after all, is the dimension check during compile-time. For only two units like Meter and Second with the derived unit Meter per Second, this might work, but for three units or even the full SI system with its seven base units Meter, Kilogram, Second, Ampere, Kelvin, Candela, and Mole, this leads to a combinatorial explosion of operators which is immense. And then this methods fails for equations as simple as
![]() |
![]() |
![]() |
let alone for such equations like Schottky-Langmuir's for the anode emission's current density
since powers and roots are not representable with this method. Admittedly, t2 can be written as t*t, and the square root can be added as another operation, but higher powers tn and their inverse are not representable.
Therefore, in the paper cited above, I presented a method that works without such useless operator overloading - for equations in the SI system, there is no dimension checking, whereas in critcal cases when incoherent units are mixed, the advantages of strong typing are kept. Basic mathematical functions like the square root and trigonometric functions are predefined operations of the numeric base types; formulae like x=x0cos(phi) with angles in degrees and radians are available in the correct dimension without type conversions. This method has successfully been in use for more than a decade now in several avionics projects under hard real-time requirements.
As the considerations above have shown, true dimensional safety can only be achieved with checks during runtime by attaching to each item its dimension as an attribute. Such a method will be presented in the next paragraphs. Although being runtime consuming, its application to hard real-time systems is not precluded a priori since dimensions may be switched off by changing only a few lines of code, thus reducing the computations to the pure numerics. I would however like to state that it's not the wrong equations that introduce the actual problems in big software systems (these can be spotted by code inspections); their causes normally are buried much earlier in wrong system or software designs, as has been shown by some recent spectacular failures.
Mars Lander's problem was the mixing of metric and British units in the communication between two CSCIs, which none of these methods would have been able to avoid since they apply only within a CSCI (CSCI: Computer Software Configuration Item - a separate program communicating with other programs via data buses or global memory).
Ariane 5 failed because of improper re-use of Ariane 4 software without previous failure analysis. This failure is often cited in order to blemish Ada. The truth is, no language would have been able to prevent this failure, since its cause was not in the program, but in the missing system analysis for Ariane 5. For Ariane 4, the exception which lead to the crash of Ariane 5, could only arise in case of a hardware problem; the error handling in the software (switching off the component with the faulty hardware) was correct for Ariane 4. Ariane 5, however, had a completely different flight profile, with the result that the same exception could arise under normal flight conditions, the error handling therefore being inappropriate. This had not been recognized since a new system analysis had not been performed in order to save cost - with the well-known consequences.
Last but not least let me point out that numeric calculations in theoretical physics are mostly done with dimensionless forms of equations, so there is no dimension checking at all.
As the introduction has shown, dimensions have to be attached as attributes to physical items. It does not suffice to consider whole exponents, as for roots we need rational numbers. Ease of use of rational numbers then also affords mixed numbers to avoid the necessity to transform n+a/b into (a+nb)/b, which is unnatural.
We define the package Rational_Arithmetics with all operations for mixed numbers. The operator
function "/" (Left, Right: Whole) return Rational;
serves us as a constructor. The selection of operators has to be done very diligently to avoid possible ambiguities in expressions like 1+3/(-7). Therefore the operator
function "/" (Left, Right: Whole) return Whole;
must not be available as well as some other operators with whole numbers as results. We can select the visible operators by defining the whole numbers in a separate package Invisible and importing only the operators desired for use with rational numbers. (As its name indicates, the package Invisible must not be used anywhere else.)
with Invisible; package Rational_Arithmetics is -- Whole Numbers subtype Whole is Invisible.Whole; function "=" (Left, Right: Whole) return Boolean renames Invisible."="; function "+" (Left, Right: Whole) return Whole renames Invisible."+"; ... function "<" (Left, Right: Whole) return Boolean renames Invisible."<"; ... -- Rational Numbers type Rational is private; -- Constructors function "+" ( Right: Whole) return Rational; function "/" (Left, Right: Whole) return Rational; -- Rational Operators function "+" (Left, Right: Rational) return Rational; function "-" (Left, Right: Rational) return Rational; function "*" (Left, Right: Rational) return Rational; function "/" (Left, Right: Rational) return Rational; ... function "<" (Left, Right: Rational) return Boolean; ... -- Mixed Operators ... private ... end Rational_Arithmetics;
How do we represent dimensioned items? It is suggestive to represent the dimension as a discriminant without defaults
type Item (kg, m, s, A, K, cd, mol: Rational) is private;
because then every variable would necessarily be given a dimension upon declaration:
Speed: Item (m => +1, s => -1, others => +0);
This fails however on the language limitation that discriminants must be discrete.
Therefore for the moment we'll do without discriminants and the indication of the dimension with declarations. We define a dimensioned item as composed of a dimension component and its numeric value (this is like a record with defaulted discriminants):
type Dimension is record m, kg, s, A, K, cd, mol: Rational; end record; type Item is record Unit : Dimension; Value: Float; end record;
We further require that numeric values be representable in as natural a way as possible, similar to e.g. v=5m/s. This leads to the following definition of the Package Unconstrained_Checked_SI.
with Rational_Arithmetics; use Rational_Arithmetics; generic type Real is digits <>; package Unconstrained_Checked_SI is type Item is private; -- Access to the pure numeric value function Value (X: Item) return Real; -- Base Units Radian: constant Item; Meter : constant Item; Second: constant Item; ... -- Operators function "+" (Right: Item) return Item; function "-" (Right: Item) return Item; function "+" (Left, Right: Item) return Item; function "-" (Left, Right: Item) return Item; function "*" (Left, Right: Item) return Item; function "/" (Left, Right: Item) return Item; function "*" (Left: Item; Right: Real) return Item; function "*" (Left: Real; Right: Item) return Item; function "/" (Left: Item; Right: Real) return Item; function "/" (Left: Real; Right: Item) return Item; function "**" (Base: Item; Exp: Whole ) return Item; function "**" (Base: Item; Exp: Rational) return Item; function "**" (Base: Item; Exp: Real ) return Item; -- Mathematics function Sqrt (X: Item) return Item; function Log (X: Item) return Item; function Exp (X: Item) return Item; function Sin (X: Item) return Item; ... Unit_Error: exception; private type Dimension is record m, kg, s, A, K, cd, mol: Rational; end record; type Item is record Unit : Dimension; Value: Real; end record; Radian: constant Item := (Unit => ( others => +0), Value => 1.0); Meter : constant Item := (Unit => (m => +1, others => +0), Value => 1.0); Second: constant Item := (Unit => (s => +1, others => +0), Value => 1.0); ... end Unconstrained_Checked_SI;
Here, in the aggregates defining the unit constants, the unary plus-operator is used as a constructor for the rational numbers 0 and 1.
With this declaration, we are able to write in very a natural style
declare Dist, Time: Item; g: constant Item := 9.81 * Meter / Second ** 2; begin Dist := 10.0 * Meter; Time := Sqrt (2.0 * Dist / g); end;
and we can be sure that the inner dimensional consistency will be conserved. Erroneous additions like Dist+Time will inevitably lead to the propagation of the exception Unit_Error. Unfortunately there is however no possibility to prevent erroneous assignments like
Length := Sqrt (2.0 * Dist / g);
where Unit_Error will not be raised. This would only be possible if the declaration of Length included the dimension Meter, which we had to omit since dimensions do not have a discrete range.
Beside the seven base units, there are a plethora of derived units like Joule, Newton, Ohm etc. We put them into a child package Unconstrained_Checked_SI.Generic_Units, which, like its parent, has of course to be generic:
generic package Unconstrained_Checked_SI.Generic_Units is Newton: constant Item := Kilogram*Meter/Second**2; -- further derived units end Unconstrained_Checked_SI.Generic_Units;
Another child package provides operations for three-dimensional vector artihmetics. This is not a linear algebraics package; all elements of a vector and a matrix must have the same dimension.
generic package Unconstrained_Checked_SI.Generic_Vector_Space is type Axis is range 1 .. 3; type Vector is array (Axis) of Item; type Matrix is array (Axis, Axis) of Item; Null_Vector: constant Vector := (Zero, Zero, Zero); Unit_Vector: constant array (Axis) of Vector := (( One, Zero, Zero), (Zero, One, Zero), (Zero, Zero, One)); Null_Matrix: constant Matrix := ((Zero, Zero, Zero), (Zero, Zero, Zero), (Zero, Zero, Zero)); Unity : constant Matrix := (( One, Zero, Zero), (Zero, One, Zero), (Zero, Zero, One)); -- Operations on vectors and matrices end Unconstrained_Checked_SI.Generic_Vector_Space;
Further (generic) children are conceivable: Unconstrained_Checked_SI.Generic_Text_IO can define file input and output, most sensibly taking into account the dimension, and Unconstrained_Checked_SI.Generic_Constants can hold natural constants.
How can we solve the aforementioned problem of erroneous assignments?
Length := Sqrt (2.0 * Time / g);
The only way is via discriminants, and thus we have to swallow the bitter pill and, for each dimension, split numerator and denominator and use them separately as discriminants! A dreadful thought. Where is the simplicity of notation?
Now, it's not so bad. Let Ada grab into her wizard's bag and she'll turn up with so-called unknown discriminants denoted by (<>), i.e. discriminants unknown to the user - and the ugliness is hidden. We rename our package into Constrained_Checked_SI and, in the visible part, only change the declaration of Item a bit:
type Item (<>) is private;
That's all we have to do. With these unknown discriminants, the user is forced to complete every declaration with an initial value; the discriminants are taken from it and are unchangeable thereafter:
Length: Item := Meter; Length := Sqrt (2.0 * Dist / g);
now inevitable leads to an exception. (To be honest, we have to admit that
Length: Item := Sqrt (2.0 * Dist / g);
is still possible, where Length takes the dimension Second from the initial value.)
The completion of the declaration of Item in the private part looks as follows, where x_N stands for the numerator, x_D for the denominator of dimension x, x=x_N/x_D:
private subtype Positive_Whole is Whole range 1 .. Whole'Last; type Item (m_N, kg_N, s_N, A_N, K_N, cd_N, mol_N: Whole; m_D, kg_D, s_D, A_D, K_D, cd_D, mol_D: Positive_Whole) is record Value: Real; end record; Meter: constant Item := (m_N | m_D => +1, kg_N => +0, kg_D => +1, ..., Value => 1.0);
[To be honest, Constrained_Checked_SI is actually implemented via Unconstrained_Checked_SI, so that the completion of the disciminated type Item looks a bit different.]
Outside, nearly everything remains unchanged, the example of above now looks like so:
declare Dist: Item := Meter; Time: Item := Sekunde; g: constant Item := 9.81 * Meter / Second ** 2; begin Dist := 10.0 * Meter; Time := Sqrt (2.0 * Dist / g); end;
One has to get accustomed to this notation, that's true, but for safety critical applications, initialisation of every variable declaration often is a mandatory requirement, coming from the experience that uninitialised variables lead to programming mistakes which are difficult to detect - and after all, it's increased safety we are after with all our effort.
We have however to admit that unknown discriminants disable the declaration of arrays:
type Vector is array (1 .. 3) of Item; -- impossible
Thus for vector arithmetics, we have to invent something special. Only the definitions for vectors are presented, matrices can be defined accordingly. Again unknown discriminants are used:
type Vector (<>) is private; Null_Vector: constant Vector; U1, U2, U3 : constant Vector; -- the unit vectors function Get (V: Vector; A: Axis) return Item; procedure Set (V: in out Vector; A: in Axis; It: in Item);
and the type is completed in the private part like Item:
type Inner_Vector is array (Axis) of Real; type Vector (m_N, kg_N, s_N, A_N, K_N, cd_N, mol_N: Whole; m_D, kg_D, s_D, A_D, K_D, cd_D, mol_D: Positive_Whole) is record Inner: Inner_Vector; end record; Null_Vector: constant Vector := (m_N | kg_N | s_N | A_N | K_N | cd_N | mol_N => 0, m_D | kg_D | s_D | A_D | K_D | cd_D | mol_D => 1, Inner => (0.0, 0.0, 0.0));
The functions Get and Put provide access to the vector components. We have now the complete capability to handle vectors of items like the scalar items themselves. Only the initialisation of vectors is still cumbersome:
V: Vector := (1.0 * U1 - 0.5 * U2 + 3.0 * U3) * Meter / Second;
In order to ease vector notation, we additionally define vector prototypes by making visible the type Inner_Vector and renaming it:
type Proto_Vector is array (Axis) of Real; -- Constructors function "*" (Left: Proto_Vector; Right: Item ) return Vector; function "*" (Left: Item ; Right: Proto_Vector) return Vector;
so that we even can write
V: Vector := (1.0, -0.5, 3.0) * Meter / Second;
As a concluding remark for this chapter, let us show in a short example how this package is used.
with Constrained_Checked_SI; package SI is new Constrained_Checked_SI (Float); with Constrained_Checked_SI.Generic_Units; package SI.Units is new SI.Generic_Units; with SI.Units; use SI, SI.Units; procedure Test_SI is g: constant Item := 9.81 * Meter / Second**2; Dist : Item := Meter; Time : Item := Second; Charge : Item := 1.0 * Coulomb: Speed : Item := 5.0 * Meter / Second; Electric_Field: Item := 10.0 * Volt / Meter; Magnetic_Field: Item := 1.0E-3 * Tesla; Force : Item := Newton; begin Dist := 10.0 * Meter; Time := SQRT (2.0 * Dist / g); Force := Charge * (Electric_Field + Speed * Magnetic_Field); end Test_SI;
Since rational arithmetic is very runtime-consuming, the methode presented above cannot be applied under hard real-time conditions without adaptations. We start out from the following considerations.
A real-time system is generally developed on a socalled host computer before being used on the target computer. Also the unit tests are performed on the host. There the correct real-time performance is irrelevant; it cannot work on a multi-user system anyway. So during development including unit test, the method presented above is used.
Only when the program is ported to the target are the dimensions switched off. To this end, another package Unchecked_SI is defined, which has the identical visible specification as one of the packages above, but in the private part the dimensions are removed, only the item's numeric value remains; all constants like Meter become the pure value 1, so they can easily be optimized away. In the package body, the dimension arithmetic is completely removed, and with it the dimension checking is of course dropped. If on the host, 100% unit test coverage is reached, at the same time also dimensional correctness of all statements is proved - the run-time checking can be switched off without detrimental effect.
with Rational_Arithmetics; use Rational_Arithmetics; generic type Real is digits <>; package Unchecked_SI is -- The visible part is unchanged. -- It holds exactly one of the two following -- declarations depending on the model used. type Item is private; type Item (<>) is private; ... private type Item is record Value: Real; end record; One: constant Item := (Value => 1.0); Radian: constant Item := One; Meter : constant Item := One; Second: constant Item := One; ... end Unchecked_SI;
It is remarkable that the completion of Item in the private part does not have a discriminant. Nevertheless for the user everything remains unchanged, declarations without initial value are still illegal when a discriminant is specified in the visible part. The implementation of child packages has possibly to be adapted; especially the child package Unchecked_SI.Generic_Text_IO cannot output dimensions, and upon input, the dimension has to be skipped.
How do we proceed when transiting from the host to the target? All we have to do is to change the instantiations of the packages SI and SI.Units from Unconstrained_Checked_SI respectively Constrained_Checked_SI to Unchecked_SI since the visible part of both packages is the same:
with Unchecked_SI; package SI is new Unchecked_SI (Float); with Unchecked_SI.Generic_Units; package SI.Units is new SI.Generic_Units;
The rest of the program (here the example Test_SI) remains completely unchanged, however big it may be.
The complete implementation of the packages Unconstrained_Checked_SI, Constrained_Checked_SI and Unchecked_SI together with some children for input and output and polynomial arithmetics and some sample programs are released under the GMGPL (Gnat modified GNU General Public License) and can be downloaded from the following address:
<home.T-Online.de/home/Christ-Usch.Grein/Ada/SI.html>
This is an extended version of a work in German that has been published in Softwaretechnik-Trends, Band 22 Heft 4, November 2002, the periodical of Ada-Deutschland, special interest group 2.1.5 Ada within Gesellschaft für Informatik, the German Informatics Society.
All about SI units and general information about the foundation of modern science and technology can be found at the National Institute of Standards and Technology.