Measuring Up - The Book of F#: Breaking Free with Managed Functional Programming (2014)

The Book of F#: Breaking Free with Managed Functional Programming (2014)

Chapter 8. Measuring Up

It is all too easy to mix up units of measurement in a long, intricate computer program. When such a mix-up occurs, the consequences can be extremely costly, even tragic. One of the most famous examples is the crash of NASA’s Mars Climate Orbiter in 1999. Investigation into the accident revealed that the crash was caused by a unit mismatch; pound-force seconds were used instead of newton seconds. This error led to an incorrect trajectory calculation and ultimately to the vehicle’s demise.

One can argue that proper testing should have detected the calculation error and thus prevented the crash, but a bigger question is whether the error would have even occurred if the programming language had enforced the proper units through its type system.

Over the years, people have tried enforcing units of measure in software systems, usually through external libraries, to varying degrees of success. F# is one of the first languages to include units of measure as a native part of its static type checking system. In addition to providing an extra level of safety beyond the basic type system, F#’s units of measure can enhance code readability by removing ambiguity about what is actually expected in the code without resorting to longer identifiers.

Defining Measures

To enable static measure checking, you first need to define a measure. Measures are type-like constructs that are decorated with the Measure attribute to represent real-world measurements. They can include an optional measure formula that describes the measure in terms of other measures. For example, the following definition creates a named unit of measure for a foot:

[<Measure>] type foot

INTERNATIONAL SYSTEM OF UNITS

F# 3.0 includes predefined measure types for the International System of Units (SI) units, including meters, kilograms, and amperes, among many others. You can find each SI unit in the Microsoft.FSharp.Data.UnitSystems namespace. Prior to F# 3.0, the SI units are included in the F# PowerPack and can be found in the Microsoft.FSharp.Math namespace.

Measure Formulas

Measure formulas allow you to define derivative measures based on one or more previously defined measures. At their most basic, formulas serve as an easy way to create synonyms for types. For instance, if you’ve defined a measure named foot and want to abbreviate it as ft, you could write this:

[<Measure>] type ft = foot

Measure formulas aren’t always quite so simple, though; they can also be used to describe more complex relationships between types, such as a measurement of distance over time. For example, miles per hour could be defined as m / h (assuming that m and h were previously defined to represent miles and hours, respectively).

Here are some of the most notable guidelines to follow when composing measure formulas:

§ You can multiply measures by separating two measures with a space or an asterisk (*) to create a product measure. For instance, torque is sometimes measured in pound-feet and could be represented in F# as:

§ [<Measure>] type lb

§ [<Measure>] type ft

[<Measure>] type lbft = lb ft

§ You can divide measures by separating two measures with a forward slash (/) to create a quotient measure. For instance, a distance over time, such as miles per hour, could be expressed like this:

§ [<Measure>] type m

§ [<Measure>] type h

[<Measure>] type mph = m / h

§ Positive and negative integral values can be used to express an exponential relationship between two measures. For instance, square feet can be expressed like this:

§ [<Measure>] type ft

[<Measure>] type sqft = ft ^ 2

Applying Measures

Once you’ve defined some measures you can apply them to values. Out of the box, F# defines measure-aware variations of the sbyte, int16, int32, int64, float, float32, and decimal primitive types. Values without measure annotations are said to be measureless or dimensionless.

To apply a measure to a constant value, you simply need to annotate the value as if the measure were a generic type parameter. For instance, you could define a length in feet and an area in square feet as follows:

> let length = 10.0<ft>

let area = 10.0<sqft>;;

val length : float<ft> = 10.0

val area : float<sqft> = 10.0

As you can see, length is bound to float<ft> while area is bound to float<sqft>.

WHERE HAVE THE STARS GONE?

Although units of measure play an important role within F#’s type system, they are erased during compilation and therefore have no impact on the compiled code. This is not to say that the measure types are not present in the compiled assembly; it means only that they’re not attached to any individual values. The net result of erasure is that units of measure can be enforced only within F# code, and any measure-aware functions or types used by assemblies written in other languages will be treated as measureless.

Measure annotations are great for constant values, but how can we apply measures to external data (such as something read from a database)? The easiest way to convert a measureless value to a measured one is to multiply it by a measured value, like this:

[<Measure>] type dpi

let resolution = 300.0 * 1.0<dpi>

Here, we define a measure representing dots per inch (dpi) and create a resolution by multiplying 300.0 by 1.0<dpi>.

For a more verbose alternative, you can use one of the seven typed WithMeasure functions from the LanguagePrimitives module. Each WithMeasure function corresponds to one of the measured primitives. Here’s how to create a new measured value using the FloatWithMeasurefunction:

[<Measure>] type dpi

let resolution = LanguagePrimitives.FloatWithMeasure<dpi> 300.0

The WithMeasure functions are a bit more explicit in their intent and are definitely more verbose. Typically, their use is reserved for when type inference fails.

Stripping Measures

The vast majority of functions do not accept unitized values, so you may need to strip measures from values. Luckily, like applying measures, stripping measures is easy.

The typical way to strip measures is to simply divide the value by a measured 1, like this:

[<Measure>] type dpi

300.0<dpi> / 1.0<dpi>

Alternatively, you can use the corresponding type conversion operator to achieve the same effect. For instance, we can strip the units from 300.0<dpi> by calling the float function as follows:

[<Measure>] type dpi

float 300.0<dpi>

Enforcing Measures

Because units of measure are part of F#’s type system, you can enforce that values passed to a function use the correct units through type annotations on the parameters. Here we define a getArea function that requires the supplied width and height to be measured in feet:

> let getArea (w : float<ft>) (h : float<ft>) = w * h;;

val getArea : w:float<ft> -> h:float<ft> -> float<ft ^ 2>

If you were to call getArea with measureless arguments as shown here, you’d receive the following error:

> getArea 10.0 10.0;;

getArea 10.0 10.0;;

--------^^^^

C:\Users\Dave\AppData\Local\Temp\stdin(9,9): error FS0001: This expression was expected to have type

float<ft>

but here has type

float

Similarly, calling getArea with arguments annotated with the wrong measure (or no measure at all) will result in a compiler error. To correctly call the getArea function, you must provide values in the proper units, like this:

> getArea 10.0<ft> 10.0<ft>;;

val it : float<ft ^ 2> = 100.0

Notice that the function’s return value is float<ft ^ 2> despite our having defined sqft as ft ^ 2. The compiler doesn’t automatically convert the measures unless explicitly instructed to do so through a return type annotation, as shown here:

> let getArea (w : float<ft>) (h : float<ft>) : float<sqft> = w * h;;

val getArea : w:float<ft> -> h:float<ft> -> float<sqft>

> getArea 10.0<ft> 10.0<ft>;;

val it : float<sqft> = 100.0

Ranges

Measured units are permissible in range expressions, but there’s a catch: You must provide a step value. To create a measured range, you could write something like this:

> let measuredRange = [1.0<ft>..1.0<ft>..10.0<ft>];;

val measuredRange : float<ft> list =

[1.0; 2.0; 3.0; 4.0; 5.0; 6.0; 7.0; 8.0; 9.0; 10.0]

Without an explicit step value, the compiler will try to create the range with the underlying type’s default, measureless value and will raise an error.

Converting Between Measures

While measure formulas allow you to create derivative units, they really aren’t flexible enough to allow arbitrary conversions between measures. To work around this limitation, you can define measure types with static members for both conversion factors and functions.

Static Conversion Factors

Defining a conversion factor on a measure type takes the same syntax as a static property. For instance, since there are 12 inches per foot, you could write something like this:

[<Measure>] type ft

[<Measure>] type inch = static member perFoot = 12.0<inch/ft>

The perFoot conversion can be accessed through the inch type like any static property. To convert from feet to inches, you would multiply a value measured in feet by inch.perFoot, as follows:

> 2.0<ft> * inch.perFoot;;

val it : float<inch> = 24.0

Notice how the compiler inferred through the multiplication operation that the result should be measured in inches. Similarly, we can convert from inches to feet by dividing a value measured in inches by inch.perFoot:

> 36.0<inch> / inch.perFoot;;

val it : float<ft> = 3.0

Static Conversion Functions

When you need more than a conversion factor, you can define static conversion functions (and their reciprocal conversions) directly on the measure types. Consistently defining the conversion functions on both measure types can help avoid confusion about where they’re defined.

To maximize code reuse, you can define the measure types as mutually recursive types by joining them together with the and keyword. Here, we define Fahrenheit and Celsius measures as mutually recursive types:

[<Measure>]

type f =

static member toCelsius (t : float<f>) = ((float t - 32.0) * (5.0/9.0)) * 1.0<c>

static member fromCelsius (t : float<c>) = ((float t * (9.0/5.0)) + 32.0) * 1.0<f>

and

[<Measure>]

c =

static member toFahrenheit = f.fromCelsius

static member fromFahrenheit = f.toCelsius

The Fahrenheit measure includes functions for converting to and from Celsius. Likewise, the Celsius measure includes functions for converting to and from Fahrenheit, but through the mutually recursive definition it can reuse the functions defined on the Fahrenheit type.

Depending on the complexity of your measure definitions or the conversion functions, you may find it cleaner to define the types independently and add the static methods later with intrinsic type extensions. This snippet shows one possible approach:

[<Measure>] type f

[<Measure>] type c

let fahrenheitToCelsius (t : float<f>) =

((float t - 32.0) * (5.0/9.0)) * 1.0<c>

let celsiusToFahrenheit (t : float<c>) =

((float t * (9.0/5.0)) + 32.0) * 1.0<f>

type f with static member toCelsius = fahrenheitToCelsius

static member fromCelsius = celsiusToFahrenheit

type c with static member toFahrenheit = celsiusToFahrenheit

static member fromFahrenheit = fahrenheitToCelsius

Here, the measure types are defined on their own (without mutual recursion) and immediately followed by the conversion functions. Since neither of the conversion functions has been attached to the measure types, we follow their definition by extending the measure types with static properties that expose the conversion functions.

Generic Measures

You’ve already seen numerous examples of how to write measure-aware functions for specific measure types, but it’s also possible to write functions against arbitrary measures using generic measures. Writing such a function is the same as for specific measure types, except that instead of using a concrete unit value you use an underscore character (_). Alternatively, or when your function accepts multiple parameters that must use the same generic measure type, you can use a generic identifier (such as 'U) instead of an underscore.

You might use generic measures when you need to perform the same operation against a variety of measures. For instance, you could write a function that computes the square of any measured float like this:

let square (v : float<_>) = v * v

Because square is defined to use a generic measure, its argument can accept any measured type. In fact, its argument can even be measureless. Here we use the square function to compute square inches, square feet, and a measureless square:

> square 10.0<inch>;;

val it : float<inch ^ 2> = 100.0

> square 10.0<ft>;;

val it : float<ft ^ 2> = 100.0

> square 10.0;;

val it : float = 100.0

Custom Measure-Aware Types

You can create your own measure-aware type by defining a generic type with a type parameter decorated with the Measure attribute. Consider the following record type:

type Point< ① [<Measure>] 'u > = { X : ② float<'u>; Y : ③ float<'u> } with

member ④ this.FindDistance other =

let deltaX = other.X - this.X

let deltaY = other.Y - this.Y

sqrt ((deltaX * deltaX) + (deltaY * deltaY))

The Point type behaves just like any other record type, except that its members are defined as generic measures. Rather than working only with measureless floats, Point includes a single measure, 'u①, that is used by X ②and Y ③. Point also defines a FindDistance function ④ that performs a measure-safe calculation to find the distance between two points. Here we create a Point instance and invoke the FindDistance function against another Point:

> let p = { X = 10.0<inch>; Y = 10.0<inch> }

p.FindDistance { X = 20.0<inch>; Y = 15.0<inch> };;

val p : Point<inch> = {X = 10.0;

Y = 10.0;}

val it : float<inch> = 11.18033989

If you try calling FindDistance with a Point that uses a different measure, the compiler will raise a type mismatch error like this:

> p.FindDistance { X = 20.0<ft>; Y = 15.0<ft> };;

p.FindDistance { X = 20.0<ft>; Y = 15.0<ft> };;

---------------------^^^^^^^^

C:\Users\Dave\AppData\Local\Temp\stdin(5,22): error FS0001: Type mismatch. Expecting a

float<inch>

but given a

float<ft>

The unit of measure 'inch' does not match the unit of measure 'ft'

Custom measure-aware types aren’t restricted to record types, either. For instance, you could define an equivalent measure-aware class like this:

type Point< [<Measure>] 'u > (x : float<'u>, y : float<'u>) =

member this.X = x

member this.Y = y

member this.FindDistance (other : Point<'u>) =

let deltaX = other.X - this.X

let deltaY = other.Y - this.Y

sqrt ((deltaX * deltaX) + (deltaY * deltaY))

Summary

Most programming languages rely on programmer discipline to ensure that measures are used correctly and consistently. One of the unique ways that F# helps developers produce more correct code is by including a rich syntax for units of measure directly within its type system.

F# not only includes predefined measure types for the International System of Units, but it also lets you define your own. You can enforce that the proper units are used in your calculations by annotating individual constant values with the appropriate measure or including them in type annotations in function definitions. Finally, you can define your own measure-aware types using a generic-like syntax.