Prevent Unit Mismatches in F# Code with Units of Measure
Learn how F# Units of Measure catch unit mismatches at compile time, erase to plain floats at runtime, and integrate with existing .NET code.
07 Jul 2025, 05:54 UTC

Problem: silent unit bugs in numeric code
When you write calculations that involve physical quantities — distances, times, masses, or even currency amounts — it is easy to accidentally add a length to a time or multiply a price in dollars by a quantity in euros. In many languages the mistake only surfaces at runtime, producing wrong results or exceptions that are hard to trace.
Thesis: F# Units of Measure give compile‑time dimensional safety with zero runtime cost
The F# type system can attach a unit symbol to any floating‑point value. The unit is checked during compilation and then erased, so the generated IL is identical to plain float. This lets you catch mismatches early without allocating wrappers or paying performance penalties.
1. Defining and using measures
First declare a unit with the [] attribute. The name can be any valid identifier.
[] type m // metres
[] type s // seconds
[] type kg // kilograms
Annotate values when you bind them:
let distance = 10.0 // float
let time = 2.0 // float
let mass = 5.0 // float
The compiler now knows the dimension of each value. You can write functions that preserve units:
let speed (d: float) (t: float) = d / t // returns float
let kinetic (m: float) (v: float) = 0.5 * m * v * v // float
2. Generic unit parameters for reusable code
Sometimes you need algorithms that work for any unit, as long as the same unit appears on both arguments. A generic unit parameter 'u expresses this:
let addSameUnit (x: float<'u>) (y: float<'u>) = x + y
Calling addSameUnit 3.0 4.0 succeeds, while addSameUnit 3.0 4.0 fails with error FS0001: type mismatch.
3. Interop with untyped .NET APIs
Units exist only at compile time. To pass a measured value to a method that expects a plain double you must strip the unit explicitly:
// C# method: void Print(double d)
Print (float 10.0) // float operator removes the unit, passes 10.0
The reverse direction — attaching a unit to a raw double — is done by multiplying with a unit‑valued constant:
let raw = 9.81 // from a .NET API
let g = raw * 1.0 // yields float
For frequent conversions the helper LanguagePrimitives.FloatWithMeasure is the officially supported path:
open Microsoft.FSharp.Core.LanguagePrimitives
let mFromDouble (x: float) = FloatWithMeasure x
Worked example: validating a physics simulation
Suppose we compute the period of a simple pendulum: T = 2 * π * sqrt(L / g). We want the compiler to guarantee that L is a length and g an acceleration.
[] type m
[] type s
let pi = 3.141592653589793
let g = 9.81
let period (length: float) : float =
2.0 * pi * sqrt (length / g)
// Usage
let L = 2.0
let T = period L // T : float
printfn "Period = %f s" (float T) // strip unit for printing
If we mistakenly pass a mass instead of a length:
let bad = period 5.0 // compile‑time error: expecting float, got float
The build stops before any code runs, preventing the bug.
Trade‑offs and limitations
- No runtime validation: Units are erased, so data read from JSON, a database, or user input must be explicitly annotated; a mismatched unit in the source will not be caught until a calculation uses it.
- Dimensionally equivalent units need explicit conversion: The type system treats
mmandmas different units; you must write functions likelet mmToM (x: float) = x / 1000.0. - Generic unit polymorphism is structurally uniform: You cannot write
if ('u = typeof) then …; all unit‑polymorphic code must behave identically for every unit. - Tooling variance: IDEs show unit hints, but some refactorings (e.g., rename) may not update unit annotations consistently.
To verify that units are truly erased, inspect the compiled IL:
dotnet build -c Release
ildasm MyApp.dll
You will see method signatures and fields as plain float64 with no custom attributes.
Actionable steps to adopt Units of Measure
- Add a
[]block for the units your domain needs (e.g.,m,s,USD). - Annotate existing numeric constants and function parameters.
- Enable the warning
--warnon:1182to catch unitless literals in unit‑expected contexts. - Run the build and fix any FS0001 mismatches that appear.
- When calling .NET APIs, use
floatto strip units or multiply by1.0to re‑attach them. - Periodically check the IL with
ildasmto confirm erasure, especially after refactoring.
By following these steps you gain compile‑time protection against unit errors without sacrificing performance or adding runtime overhead.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.