Categories
Tech

Types With Units

The Problem

I have been working on an application that displays various types of data, and some of the data may be displayed in more than one unit of measure, according to the user’s preference. The diversity of units is not just on the output side. The application consumes a number of input formats: an angle value may be in degrees or radians; a distance value may be in meters or feet.

To cope with all these differences, we created an abstraction layer that converts the various incoming data formats into a common format. Only when we need to display a value in a certain unit of measurement do we perform the units conversion to the desired format. There was a number of occurrences of magic numbers to convert values from one unit to another:

// C#
//Convert from ft/min to m/s
speed = (u.VerticalSpeed * (1 / (3.28084 * 60))) + "  m/s";

Obviously, poor practice that I had to clean up.

More frequently there were no magic numbers, but named constants:

// C#
altitudeValue = Constants.METERS_TO_FEET * u.Position.Altitude;

An improvement, for sure, but there’s still a problem. The units are implied here by the calculation, but if I run across u.Position.Altitude elsewhere in the program, how do I, as the code maintainer, know that Altitude is in meters?

We could depend on convention: Always use SI units. Distances are meters, speeds are meters per second, and so on. Still, from time to time, someone on the team would forget. In fact, the VerticalSpeed calculation above is wrong: VerticalSpeed is in meters per second, not feet per minute. How can we be sure that no one forgets what the units are?

One convention I employ frequently is to add a suffix to the variable/method that indicates its units:

// C#
speed = u.VerticalSpeedMeters + " m/s";

It works, but can get a little verbose. It becomes necessary to attach a units suffix to every single variable, even when there is no units conversion going on.

Furthermore, every time there is a units conversion, we have to type out the calculation. While simple enough, that’s code duplication. How can we reduce duplication?

I have recently been more scrupulous about encapsulating a frequently performed calculation—even a simple one—within a method named so that it describes the operation. In fact, in doing so, I kill two birds with one stone: reduce code duplication and eliminate the need for a comment because the method name already tells you exactly what it’s doing:

// C#
lat = ConvertRadiansToDegrees(latitudeRadians);

That is better, but it feels a little too C-like. Yeah, it’s a matter of taste, but I don’t like it.

Finally, there’s nothing that prevents me from doing this:

// C#
lat = ConvertRadiansToDegrees(accelerationMeters);

The compiler cannot help me. First, the correct units of acceleration are meters/second², not meters. Second, I am handing an acceleration value to a function that expects an angle value. The compiler does not catch the mismatch because both angles and accelerations are doubles—as are distances, speeds, flow rates, angular speeds, etc. Can we somehow use the type system to enlist the compiler’s help?

The Solution

So then, here are the forces our solution has to balance:

  • Eliminates magic numbers
  • Encapsulates the units conversion algorithm
  • Communicates the units of measure when we care, but does not clutter the code when we don’t
  • Has a less C-like, more OO feel
  • Uses the type system to help prevent accidental mismatches of different measurement types

What if we use custom types for each measurement type rather than a generic double? For example, what if we could do this for angles?

// C#
Angle lat = new Angle(Math.PI / 2.0);
double rad = lat.Radians;
double deg = lat.Degrees;

Well, in fact, we can: using classes—or rather C# structs in our case since that is the C# way for value types.

Let’s take the TDD approach and write our test first.

// C#
namespace UnitsOfMeasureTests.Units
{
    [TestFixture]
    class AngleTest
    {
        private const double OneRadianInRadians = 1.0;
        private const double OneRadianInDegrees = 180.0 / Math.PI;

        private const double OneDegreeInDegrees = 1.0;
        private const double OneDegreeInRadians = Math.PI / 180.0;

        [Test]
        public void TestRadiansProperty()
        {
            Angle angle;

            angle = new Angle(OneRadianInRadians);
            Assert.That(angle.Radians, Is.EqualTo(OneRadianInRadians));
            angle = new Angle(OneDegreeInRadians);
            Assert.That(angle.Radians, Is.EqualTo(OneDegreeInRadians));
        }

        [Test]
        public void TestDegreesProperty()
        {
            Angle angle;

            angle = new Angle(OneRadianInRadians);
            Assert.That(angle.Degrees, Is.EqualTo(OneRadianInDegrees));
            angle = new Angle(OneDegreeInRadians);
            Assert.That(angle.Degrees, Is.EqualTo(OneDegreeInDegrees));
        }
    }
}

Now the implementation:

//C#
namespace UnitsOfMeasure.Units
{
    public struct Angle
    {
        public readonly double Radians;
        public readonly double Degrees;

        private const double DegreesPerRadian = 180.0 / Math.PI;

        public Angle(double radians)
        {
            this.Radians = radians;
            this.Degrees = radians * DegreesPerRadian;
        }
    }
}

That’s not bad, but we can do better. Consider this:

// C#
Angle latitude = new Angle(latValue);

How do I know at a quick glance whether latValue is in radians or degrees? I have no idea unless I look at the constructor to find out what units it expects. To make the the code communicate a little better, let’s employ the named constructor idiom. First, we change our test:

// C#
namespace UnitsOfMeasureTests.Units
{
    [TestFixture]
    class AngleTest
    {
        private const double OneRadianInRadians = 1.0;
        private const double OneRadianInDegrees = 180.0 / Math.PI;

        private const double OneDegreeInDegrees = 1.0;
        private const double OneDegreeInRadians = Math.PI / 180.0;

        [Test]
        public void TestRadiansProperty()
        {
            Angle angle;

            angle = Angle.InRadians(OneRadianInRadians);
            Assert.That(angle.Radians, Is.EqualTo(OneRadianInRadians));
            angle = Angle.InRadians(OneDegreeInRadians);
            Assert.That(angle.Radians, Is.EqualTo(OneDegreeInRadians));

            angle = Angle.InDegrees(OneRadianInDegrees);
            Assert.That(angle.Radians, Is.EqualTo(OneRadianInRadians));
            angle = Angle.InDegrees(OneDegreeInDegrees);
            Assert.That(angle.Radians, Is.EqualTo(OneDegreeInRadians));
        }

        [Test]
        public void TestDegreesProperty()
        {
            Angle angle;

            angle = Angle.InRadians(OneRadianInRadians);
            Assert.That(angle.Degrees, Is.EqualTo(OneRadianInDegrees));
            angle = Angle.InRadians(OneDegreeInRadians);
            Assert.That(angle.Degrees, Is.EqualTo(OneDegreeInDegrees));

            angle = Angle.InDegrees(OneRadianInDegrees);
            Assert.That(angle.Degrees, Is.EqualTo(OneRadianInDegrees));
            angle = Angle.InDegrees(OneDegreeInDegrees);
            Assert.That(angle.Degrees, Is.EqualTo(OneDegreeInDegrees));
        }
    }
}

Then we change our implementation to match by making the constructor private and adding the two named constructors InRadians and InDegrees:

// C#
namespace UnitsOfMeasure.Units
{
    public struct Angle
    {
        public readonly double Radians;
        public readonly double Degrees;

        private const double DegreesPerRadian = 180 / Math.PI;

        public static Angle InRadians(double radians)
        {
            return new Angle(radians);
        }

        public static Angle InDegrees(double degrees)
        {
            return new Angle(degrees / DegreesPerRadian);
        }

        private Angle(double radians)
        {
            Radians = radians;
            Degrees = radians * DegreesPerRadian;
        }
    }
}

Now it is perfectly clear whether we are initializing a new Angle object with a radian value or a degree value.

So there it is. We now have an Angle value type that eliminates magic numbers and code duplication by encapsulating the units conversions within the struct. Instead of being constrained to a primitive double value that could be confused with any other double, we can now lean on the compiler for help. Finally, instead of having to append units suffixes all over the place, we pass around an object that can give us its value in whatever units we desire only when the units matter:

// C#
Angle latitude = Angle.InRadians(message.CurrentPosition.Latitude);
latTextBox.Value = String.Format("{0:0}°", latitude.Degrees);

Case In Point: NASA World Wind

I had already dreamt up my design independently, but I came to find out that NASA’s World Wind has an Angle class that does things similar to how our Angle struct above does. In fact, it has some additional features, such as arithmetic and trigonometric functions, which I should like to add to my Angle type in future posts.

3 replies on “Types With Units”

         
public static Angle InRadians(double radians) { return new Angle(radians); }
public static Angle InDegrees(double degrees) { return new Angle(degrees / DegreesPerRadian); }

My skill is fuzzy. Why are you generating a whole new instance of Angle with these methods?

@Eric, because the named constructors, InRadians and InDegrees, are static, there is no instance of Angle. The whole point of these functions is to create “a whole new instance of Angle”, but to do so in a way that instantly tells the reader the units of the value Angle is being initialized with, instead of making him go look up the Angle constructor to figure that out.

In other words, you wouldn’t use them this way:

Angle a = new Angle(0.5);
Angle b = a.InDegrees(45);

You’d use InDegrees or InRadians to create b to begin with:

Angle b = Angle.InDegrees(45);

Does that make sense?

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.