Skip to main content

F# Units of Measure Enforce Dimensional Consistency at Compile Time

Most type systems stop at the shape of your data. A function that takes a float and returns a float will happily accept seconds, dollars, or kilograms, and nothing in the compiler objects when you add meters to milliseconds. F# is one of the few mainstream languages that closes that gap. Units of measure are a first class part of the type system, and they let the compiler reject arithmetic that is dimensionally nonsense before a single line of code runs.

The syntax is deliberately lightweight. You declare a unit with an attribute and a type name, so a measure like cm is introduced with a Measure attribute on a type. From there you annotate literals and values: twelve centimetres is written as a number followed by the unit in angle brackets. A rate is a compound expression such as meters per second, and the number one on its own marks a dimensionless quantity, so something like one over seconds is a perfectly ordinary unit expression. Once values carry units, the type of a length is a floating point number parameterised by that unit, and the compiler tracks how units combine through multiplication and division the same way it tracks generic type parameters.

What that buys you is immediate. Adding a value measured in meters to a value measured in seconds does not produce a warning or a runtime conversion. It fails to compile, because the two unit parameters do not match. That is exactly the class of mistake behind some of the most expensive software failures on record, including the loss of a spacecraft to a metric and imperial mismatch. F# pushes that check forward into the editor, where it costs seconds instead of missions.

There are some precise boundaries worth knowing. Units of measure can be attached to any type, but only floating point types, signed integral types, and decimal types actually support dimensioned quantities, so the feature is meaningful on those primitives and on aggregates that contain them. The reason is that a unit of measure is a type level annotation with no runtime representation. When the compiler emits code for floating point values, the units are eliminated entirely, which means they are gone at run time. You cannot write a function that prints the units of a value, and you cannot inspect them later for logging or validation. If a design depends on knowing at run time what unit a number carries, that design cannot be expressed this way, and you should store the unit as data alongside the value instead.

The feature also composes into generic code, which is where it gets genuinely powerful. A function can be typed over a generic unit, so you can write a single addition helper that sums any two values sharing the same unit, and the compiler guarantees the units match without you spelling them out. Container types can carry generic units too, by applying the Measure attribute to a type parameter, which means a vector of velocities is a different type from a vector of accelerations and cannot be mixed up by accident. Each instantiation keeps its own identity, so the type safety survives all the way through your abstractions.

For real projects there are two pieces of infrastructure that make this practical. The first is the SI unit library in the FSharp.Data.UnitSystems.SI namespace, which ships definitions for standard units in two flavours: short symbol forms such as m for meter, and full name forms such as meter. Picking one convention per codebase keeps things readable. The second is the interop story. Data arriving from the outside world, from JSON payloads, database columns, or .NET APIs written in other languages, has no units attached. You convert a dimensionless value into a unit carrying value by multiplying by one annotated with the target unit, or by using an explicit helper from the LanguagePrimitives module such as FloatWithMeasure. Going the other way, the standard float conversion function strips units when you hand a value to an API that only understands plain numbers. That makes units of measure a boundary discipline: annotate at the edges, then let the compiler hold the invariant in the middle.

Historical context explains why F# is unusual here. Units of measure came out of work at Microsoft Research Cambridge, with Andrew Kennedy contributing to the design of the feature, and later releases extended it further, including provided units of measure in F# 3.0 in November 2012. It remains rare because the cost is real: annotations add friction, generic units take a little practice to read, and external libraries will never speak this language back to you. The payoff is a whole category of bug that simply cannot be written. For anything involving engineering, finance, physics, or measurement heavy domain logic, that trade is usually worth making.

We use cookies for ads and analytics.