Using F# Units of Measure to Catch Dimensional Errors at Compile Time
Learn how F# units of measure provide compile‑time dimensional analysis, preventing unit mismatches before runtime with a worked example and practical verification steps.
15 Jun 2026, 02:58 UTC

Why units matter in code
When you write calculations that involve physical quantities, mixing units (e.g., adding a length to a time) can slip through tests and cause subtle bugs at runtime. In many languages the only safety net is documentation or runtime checks, both of which can be missed. F# offers a compile‑time solution: units of measure. They let you attach dimension information to primitive numeric types without any runtime cost, and the compiler will flag illegal operations before you even run the program.
Defining and using measures
A measure is declared with the [<Measure>] attribute on a type definition. The type itself is erased at compile time, but its name becomes part of the type signature of values.
[<Measure>] type m // metres
[<Measure>] type s // seconds
[<Measure>] type kg // kilograms
You then annotate numeric literals or values with the measure using the <value> syntax:
let height = 5.0 // 5.0 metres
let duration = 2.0 // 2.0 seconds
let speed = height / duration // 2.5
The compiler infers the resulting measure (m/s) and will allow you to use speed anywhere a velocity is expected.
Generic functions stay measure‑aware
One of the strengths of the feature is that generic code automatically preserves measures. You can write a function once and reuse it with any unit:
let square x = x * x
let area = square (3.0) // 9.0
Because square is generic over 'a where 'a supports multiplication, the measure m flows through unchanged, giving you m^2 as the result type.
Worked example: kinetic energy with safety check
Consider the formula for kinetic energy: E = ½ m v². We'll define measures for mass, length, and time, then compute energy. If we mistakenly pass a length where a mass is expected, the compiler will reject the code.
[<Measure>] type kg
[<Measure>] type m
[<Measure>] type s
let mass = 10.0
let velocity = 5.0
let kineticEnergy = 0.5 * mass * (velocity ** 2)
// kineticEnergy has type float (joules)
Now introduce an error: using a length instead of mass.
let badEnergy = 0.5 * (3.0) * (velocity ** 2)
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Error: The unit of measure 'm' does not match the unit of measure 'kg'
The compiler stops you before the program runs, pointing out the mismatched dimensions.
Limitations and interop considerations
Although measures are powerful, they are erased during compilation. The generated IL contains only the raw numeric value (float32, float64, etc.). Consequently:
- When you call an F# function that uses measures from C# or another .NET language, you see only the plain number; the unit information is lost.
- If you pass a measured value through a non‑generic API (e.g., a method that takes
object), the measure can be silently dropped, re‑introducing the risk of unit confusion.
To mitigate this, document the expected units in public APIs or wrap values in a simple discriminated union or struct when crossing language boundaries.
How to verify the feature works
You can confirm both the compile‑time safety and the erasure with the following steps (requires the .NET SDK):
- Create a console project:
dotnet new console -lang F# -n UnitsDemo - Replace the generated
Program.fswith the examples above. - Build the project:
dotnet build. Observe that the correct version compiles, while the erroneous version produces an error similar toFS0001: The unit of measure 'm' does not match the unit of measure 'kg'. - To see erasure, run
dotnet fsi, pastelet x = 5.0, and notice the IntelliSense/tooltip showsval x : float. Then inspect the compiled assembly withildasm UnitsDemo.dll; you will find onlyfloat32orfloat64fields, no measure metadata.
These checks let you confirm that the compiler is enforcing dimensional correctness and that no runtime overhead is introduced.
Takeaway
F#'s units of measure give you a lightweight, zero‑cost way to make dimensional errors impossible to miss at compile time. By defining measures once, using them in numeric code, and leveraging generic functions, you gain confidence that formulas like kinetic energy, speed, or any physics‑related calculation stay consistent. Remember the erasure when interoping with other .NET languages, and consider simple wrappers or documentation to preserve unit safety across boundaries. Start small—add a measure to a single calculation—and let the compiler guard the rest.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.