Units of Measure

This week's Elements build adds a major new feature that I personally have been dreaming of adding to Oxygene for well over a decade; we just never got around to it. Well, now we did.

One of the great things about Object Pascal (and other true OOP languages) is that it is strongly typed. You cannot just assign a String to a Button, the compiler won't let you. This leads to safer code that is validated at compile time rather than at runtime. But there's one area where Object Pascal has let us down until now, and that is numbers. Numbers can mean a great many things. 100 can be a lot, or really little, depending on context.

Let's just take one common example: fEvent.WaitFor(aTimeout: Integer);. Ok, cool, but what unit is this integer in? Will it be too quick if I pass 1 because it's milliseconds, or will it wait forever if I pass 1000, because it expects seconds? Who knows 🤷‍♀️. Maybe it's in the documentation. But the compiler certainly has no idea, and can't help you.

Until now.

Oxygene 13 adds two new fundamentals to the language: dimensions and units.

A dimension is the abstract concept of something that can be measured by a number. For example, distance. A unit is a concrete, well-defined way to measure a dimension. For example, meters. Or furlongs. Or football fields, if you're American.

type
  Distance = public dimension(Double);
  Meters = public unit(Distance) as m;
  Yards = public unit(Distance) as yd;

This code defines a new dimension that represents distances, as well as two concrete units.

var x: Meters := 5;
...
var y: Yards := x;

In the past, we'd have a problem here that we'd not find out about until someone runs our app and notices that the numbers are off by 0.9144. Not ideal. But now that we have strongly-typed units, the compiler knows your code is about to mess up, and can do something about it.

But what? The simplest idea would be to warn (or error) to let you know about your mistake. But we can do better than that! What if instead Yards were defined as follows?

type
  Yards = public unit(Distance) as yd = 0.9144m;
 

Now the compiler can do more than just err. The compiler can actually make your code work as intended by converting from meters to yards, for you.

Let's go back and redefine our WaitFor method as such:

method WaitFor(aTimeout: Milliseconds);

The compiler now knows what exact unit the method expects, and can enable us to call it with the unit that we find convenient when calling it:

var DefaultTimeout: Seconds := 30;
...
fEvent.WaitFor(DefaultTimeout);

The call site is clearly stating its intention – not to wait for 30 whatevers, but for thirty seconds, specifically. The method clearly states that it works with milliseconds. This allows the compiler to do the rest, and convert your "30" into "30000".

Full Unit Syntax

There are four parts to a unit definition:

type
  Distance = public dimension(Double);
  LightYears = public unit(Distance) as ly = 9 460 730 472 580 800m;
  • First, the name.Lightyears defines a new strong numerical type that is at runtime backed/represented as a 64-bit floating-point double.
  • Then, Distance establishes the unit as part of the Distance dimension. Units of the same dimension can be compatible with each other (as we've seen above for Meters/Yards and Seconds/Milliseconds. Even if a unit mismatches, the compiler knows whether the two values even represent the same concept or not.
  • ly provides an (optional) shorthand form the unit that can be used in constant expressions: DistanceTraveled := 2ly;
  • Finally, a conversion path can be provided between different units in the same dimension. Here, we tell the compiler that one light year is about 9 and a half quadrillion meters. That's a long way home.

Any two units for which the compiler can find a conversion path (whether direct or indirect) can simply be assigned to each other. Assigning, e.g., a mile to a centimeter, and the compiler might go from mile to yard, to meter, to centimeter, if the following units are defined:

type
  Centimeters = public unit(Distance) as cm = 0.01m;
  Meters = public unit(Distance) as m;
  ...
  Yards = public unit(Distance) as yd = 0.9144m;
  Miles = public unit(Distance) as mi = 1760yd;

You can even do var Distance := 5km+5m+1mi.

Note: If you like, you can define multiple units in the same dimension w/o a well-defined conversion factor. You will still get the benefits of unit type safety; you just won't get automatic conversion. For example:

type
  Refrigerator = public unit(Volume);

No one except TV newscasters really knows what "the size of 10 refrigerators" actually means, but at least now it won't accidentally be mistaken for a proper unit such as, say, cubic meters 🙃.

Another cool unit to have is

type
  Temperature = public dimension(Double);

  Kelvin = public unit(Temperature) as K;
  Celsius = public unit(Temperature) as °C = value + 273.15 K;
  Fahrenheit = public unit(Temperature) as °F = (value + 459.67) * 5 / 9 K;
  Rankine = public unit(Temperature) as °R = value * 5 / 9 K;

Now you can do:

var c := 35ºC;
writeLn(#"c {c}"); // 35 ºC
writeLn(#"c {c as Kelvin}"); // 308.15 K

Or, if you must:

  writeLn(#"c {c as Fahrenheit}"); // 95 °F

And yes, you can use º in a unit name, just fine.

Dimension Relationships

Things become more interesting when more than one dimension is defined, and we teach the compiler the relationship between them:

  Time = public dimension(Double);
  Speed = public dimension(Distance/Time);
  Acceleration = public dimension(Speed/Time);
 
  ...
 
  Seconds = public unit(Time) as s;
  Minutes = public unit(Time) = 60s;
  Hours = public unit(Time) = 60 Minutes;
  ..
  MetersPerSecond = public unit(Speed) as mps = m/s;
  KilometersPerHour = public unit(Speed) as kmh = km/Hours;
  MilesPerHour = public unit(Speed) as mph = mi/Hours;

We can now do

var lDistance := 5km;
var lTime := 30s;
var lSpeed: KilometersPerHour := lDistance/lTime;

and despite lTime being provided in seconds, the result will be in km/h!

More importantly, even without the explicit type, the compiler would derive that var lSpeed := lDistance/lTime should be a Speed type (it would just pick a different default unit, probably).

This becomes more fun, the more complex the relationships get. For (maybe a bit of a contrived) example, here x will be strongly typed as GravitationalConstantUnits.

  var l := 2.0m;
  var mass := 500.0g;
  var time := 3.0Hours;

  var x := l*l*l/(mass*time**2);

simply because the compiler has been told that unit is defined as m**3/(kg*s2):

type
  GravitationalConstantUnits = public 
 unit(GravitationalConstantDimension) = m**3/(kg*s2);

Unit Conversions

As seen above, a unit can be defined in relation to a different unit of the same dimension (or even a different one, in some cases). This can take several shapes:

With a simple conversion factor

Simply using a unit expression of a sibling unit, with a value, can define a conversion factor. The value will be understood to represent 1 of the new unit. One km is 1000m. Once cm is one hundredth of a Meter.

  Meters = public unit(Distance) as m;

  Centimeters = public unit(Distance) as cm = 0.01m;
  Kilometers = public unit(Distance) as km = 1000m;

  Feet = public unit(Distance) as ft = 0.3048m;

With a more complex expression:

For some units, conversion not as simple. You can define a more complex conversion expression using the value keyword to express how a value of the new unit is converted to a different sibling unit. For example, by adding 273.15 K to the ºC value to go to Kelvin.

  Kelvin = public unit(Temperature) as K;
  Celsius = public unit(Temperature) as °C = value + 273.15 K;
  Fahrenheit = public unit(Temperature) as °F = (value + 459.67) * 5 / 9 K;

Once defined, these work both ways, e. g from ºC to ºF and back from ºF to ºC.

Via Base Units

For units derived from other base units*, you can also use the base unit in the expression, e.g.:

type
  Area = public dimension(Distance**2);
  SquareMeters = public unit(Area) as m² = m**2;

Here, the SquareMeter unit is not defined by relation to a different Area unit, but as the square (or really, multiplication of two) Distance units, Meters. Note how this matches the definition of the dimension, which is declared as Distance**2. (** is the power operator in Oxygene, e.g. Distance²)

As Reciprocal Relationship

Finally, a unit (and its dimension) can be defined as the reciprocal (or inverse) of a different unit. For example, frequency is the number of <somethings> per unit of time:

type
  Frequency = public dimension(1/Time);

  Hertz = public unit(Frequency) as Hz = 1/s;

* We consider a base unit the unit that all others are derived from. E.g. in the examples above, Meters is the base unit, while Centimeters or Yards are derived from it. In many cases, the choice of base unit is arbitrary; one could just as easily have picked Millimeter as the base.

The same goes for dimensions. We have Distance and Time defined as base dimensions, while Speed is defined as the quotient of Distance and Time. That is the intuitive way to do it, but as far as the compiler is concerned, we could have declared Speed as a base unit Distance = dimension(Speed*Time). It'd be a lot more confusing, but just as valid.

As a final note here, the unit system is smart enough to understand the relationship between exponential expressions and multiplications. E.g. while area is defined as Distance**2 (i.e. Distance²), it knows that just means "Distance * Distance", i.e. any product of two Distances will qualify as an Area, and vice versa; the same goes for e.g. **3.


Units in Elements RTL

Units come to you in Oxygene 13.0.0.3101 in two ways.

First, it is a language feature that you can use to define your own units as you need.

Secondly, and I think more interestingly for most users, Elements RTL comes with a huge list of predefined units in the RemObjects.Elements.RTL.Units namespace. Just reference the library (you should anyway), and add the new namespace to your uses clause, and you have immediate access to several dozen dimensions and units, including all the examples shown here, many more derived units in the same dimensions (eg mm, cm, km), as well as units for many common physics, chemistry and electro-technical domains such as Mass, Force and Pressure, Speed, Acceleration and Momentum, Power and Energy, Voltage, Amperage and Resistance, Radioactivity, and many more.

If you define your own units, we would strongly sugest to build on what's in RemObjects.Elements.RTL.Units. But you can, of course, also just define your own, independent unit space.

Finally, if you think we missed any obvious or (more likely, given the broad coverage) not-so-obvious units or dimensions for your favorite domain in our default list, please do not hesitate to let us know, and we will expand the list.

You can have a look at Units.pas to get inspired by what's already there and to consider how you can apply it to your projects, as well as to see examples for more complex and esoteric unit definitions, such as KilogramSquareMetersPerSecond.

Get Build .3101 or later of Oxygene 13 now.

Units are new in build .3101. Get it now, as a free trial download or as a free update for all active subscribers!