As I’ve been outlining the things I want to write about in regards to F# there are only a few topics that excite me more than units of measure. If you’ve ever had to write code that deals with different measurement units (and who hasn’t?) this feature can quickly become one of your best friends. I really struggled with where to include this in the series but ultimately decided that it made the most sense to keep it near primitive data types since they’re so closely related.
The Case for Units of Measure
My current project at work involves a bit of image manipulation. If we were only working with pixels this wouldn’t be an issue but we actually need to translate some values between inches and pixels. This, of course, means we have three distinct units of measure: inches, pixels, and dots per inch for conversion. Keeping track of which types and methods require pixels versus those that require inches can be troublesome to say the least. We can mitigate the problem a bit through naming conventions but simply calling something widthInInches does not guarantee that the supplied value is actually in inches.
This example is pretty insignificant in terms of impact on the world but using the wrong unit of measure can have devastating consequences. Wouldn’t it be nice if the compiler could enforce using the correct measurement? Units of measure in F# do just that.
Before we dive in to defining units I think it’s helpful to start from an example that doesn’t have them. Consider the following:
let convertToPixels inches resolution = inches * resolution let convertToInches pixels resolution = pixels / resolution let resolution = 150.0 let pixels = convertToPixels 8.0 resolution let inches = convertToInches pixels resolution
In this simple example we have functions to convert between inches and pixels. The parameter names guide us but there’s nothing enforcing any rules about the values. How can we be sure that 8.0 and 150.0 are actually inches and dots per inch? The answer, of course, is to define and apply some units.
Introducing Units of Measure
Units of measure are simply specially annotated types that can be associated with numeric (both signed integral and floating point) values. Defining a basic unit of measure is easy; just define an opaque (memberless) type and decorate it with the Measure attribute. For instance, we can define dpi, inches, and pixels as follows:
[<Measure>] type dpi [<Measure>] type inch [<Measure>] type px
If your application requires SI units (International System of Units) you can save yourself some time as these units are already defined. For F# versions prior to 3.0 you can find them in the F# PowerPack on CodePlex. In F# 3.0 they are included in the FSharp.Core assembly and found under the Microsoft.FSharp.Data.UnitSystems.SI.UnitNames and Microsoft.FSharp.Data.UnitSystems.SI.UnitSymbols namespaces.
Identifying values of a particular unit is similarly trivial:
let dpiValue = 150.0<dpi> let inchValue = 8.0<inch> let pxValue = 1200.0<px>
Microsoft recommends using only using units of measure with floating point expressions to avoid potential conversion problem. I’ll be following that convention throughout this article.
In the above example, each value is qualified by a particular measure. With this knowledge we can modify our conversion code to enforce that only properly qualified values can be supplied.
[<Measure>] type dpi [<Measure>] type inch [<Measure>] type px let convertToPixels (inches : float<inch>) (resolution : float<dpi>) = float(inches * resolution) * 1.0<px> let convertToInches (pixels : float<px>) (resolution : float<dpi>) = float(pixels / resolution) * 1.0<inch> let resolution = 150.0 let pixels = convertToPixels 8.0 resolution let inches = convertToInches pixels resolution
The function signatures have been updated with type annotations to qualify the parameters with the required unit of measure. If we attempt to run this code though the compiler will raise an error because the values being passed to the functions haven’t been qualified. To continue we just need to update the value definitions with the correct units:
let resolution = 150.0<dpi> let pixels = convertToPixels 8.0<inch> resolution let inches = convertToInches pixels resolution
Changing Units of Measure
Not all code is aware of measures so there will be plenty of occasions when we’ll need to either add or remove units of measure from an existing value and there are a couple of approaches to both.
Adding Units of Measure
If your data is coming from an external source you’ll need to add units to it to use it with your measure aware code. The easiest way to add units is to take the raw value and multiply it by some value with the desired unit.
> 150.0 * 1.0<dpi> val it : float<dpi> = 150.0
Alternatively we can use a library function from the LanguagePrimitives module in FSharp.Core.
> LanguagePrimitives.FloatWithMeasure<dpi> 150.0 val it : float<dpi> = 150.0
Removing Units of Measure
If your data needs to be used by something that isn’t measure aware you’ll need to strip away the units. Like with adding units, removing units can be done with a simple mathematical operation. To easily remove units just multiply the measured value by another value of the same units.
> 150.0<dpi> / 1.0<dpi>;; val it : float = 150.0
We can also remove units by passing the measured value to the conversion function that corresponds to it’s underlying type.
> float 150.0<dpi>;; val it : float = 150.0
Relating Units of Measure
In the physical world there is often a correlation between different units of measures. F# allows us to express those relationships naturally. Continuing with the theme of image processing let’s look at dots per inch. So far we’ve used a unit named dpi to define dots per inch. On it’s own this is already a huge improvement over a naming convention but we can express the concept much more eloquently by adding another measure and defining relationships between them.
[<Measure>] type px [<Measure>] type inch [<Measure>] type dot = 1 px [<Measure>] type dpi = dot / inch
We added in the dot measure with a formula that identifies a dot as being equivalent to a pixel (the 1 is optional, see the rules section below for more information). We also modified the dpi measure with a formula defining it as being dots divided by inches. These two changes have a profound impact on our conversion code. By defining the formulas and including a type annotation for the function return value the compiler has enough information to convert the return values from float<dpi inch> and float<px/dpi> to float<px> and float<inch>, respectively.
let convertToPixels (inches : float<inch>) (resolution : float<dpi>) : float<px> = inches * resolution let convertToInches (pixels : float<px>) (resolution : float<dpi>) : float<inch> = pixels / resolution
Measure Formula Rules
There are some rules to keep in mind when writing unit formulas. Some highlights:
- Positive and negative integral powers are supported
- Spaces (or *) between measures indicate a product
- A / character between measures indicates a quotient
- 1 is allowed to express a dimensionless quantity or with other units
Runtime Implications
Units of measure are a feature of F#’s static type checking logic and are not included in the compiled code. The implication is that except in certain scenarios we can’t write anything to detect units of measure at runtime.
Next Steps
This article introduced units of measure and demonstrated how they can improve the quality of your code but we’ve only examined how to apply them to simple values and pass specific units to functions. In the next article in this continuing series we’ll look at some other ways to use units of measure including:
- Adding static members to measures
- Using generic measures
- Defining custom measure-aware types
3 comments
Comments are closed.