Enforcing Dimensional Safety in F# with Units of Measure
F# Units of Measure attach compile-time dimension metadata to numeric types, preventing unit mismatches before runtime.
29 May 2026, 17:31 UTC

The Problem: Silent Unit Mismatches
\nNumeric calculations in most languages treat all numbers as interchangeable. A function expecting meters receives seconds, a currency conversion uses raw decimals without tracking dollars versus euros, and the compiler stays silent. Bugs surface only at runtime—or in production.
\nF# Units of Measure attach compile-time dimension metadata to numeric types. The compiler rejects mismatched units before code runs, while erasing the metadata at runtime for zero overhead.
\nPrerequisites
\n- \n
- F# 5.0 or later (included with .NET SDK 5.0+) \n
dotnetCLI available on PATH \n- Basic F# syntax familiarity \n
- A project or script file (
.fsor.fsx) \n
Define Measures and Annotate Values
\nCreate a new script file units-demo.fsx and add measure definitions using the [<Measure>] attribute:
[<Measure>] type m\n[<Measure>] type s\n[<Measure>] type kg\n\nlet distance = 100.0<m>\nlet time = 9.58<s>\nlet mass = 70.0<kg>\nEach measure creates a distinct type: float<m>, float<s>, and float<kg> are incompatible with each other and with raw float.
Write Dimension-Aware Functions
\nFunctions declare expected units in their signatures. The compiler enforces them at call sites:
\nlet speed (d: float<m>) (t: float<s>) : float<m/s> = d / t\n\nlet momentum (m: float<kg>) (v: float<m/s>) : float<kg*m/s> = m * v\n\n// Valid usage\nlet v = speed distance time // float<m/s>\nlet p = momentum mass v // float<kg*m/s>\nCompound units like m/s and kg*m/s are inferred automatically from arithmetic operators.
Observe Compile-Time Rejection of Mismatches
\nAttempting to add incompatible units produces a compile error. Add this line to the script:
\n// This line will not compile\nlet invalid = distance + time\nRun the script with F# Interactive to see the error:
\ndotnet fsi units-demo.fsx\nWhere to run: Terminal in the script directory. Permissions: Standard user. Expected check: Compiler emits error FS0001: \"The unit of measure 'm' does not match the unit of measure 's'.\" Risk: None—this is a compile-time check only.
\nHandle Conversions Explicitly
\nNo implicit casting exists. Define conversion factors as measured constants:
\nlet metersPerKilometer = 1000.0<m/km>\nlet kilometersPerMile = 1.60934<km/mi>\n\nlet toMeters (km: float<km>) : float<m> = km * metersPerKilometer\nlet toKilometers (mi: float<mi>) : float<km> = mi * kilometersPerMile\nRaw literals require annotation when used with measured values:
\n// Correct: annotate the constant\nlet halfDistance = distance * 0.5<m>\n\n// Error: raw float cannot multiply float<m>\n// let wrong = distance * 0.5\nVerify Runtime Behavior
\nUnits are erased in compiled IL. The runtime values are plain numbers. Confirm this by printing results:
\nprintfn \"Speed: %f m/s\" (float v)\nprintfn \"Momentum: %f kg*m/s\" (float p)\nprintfn \"Distance in meters: %f\" (float (toMeters 5.0<km>))\nRun again with dotnet fsi units-demo.fsx. Output shows raw numeric values with no unit metadata attached.
Work with Generic Code
\nUnits integrate with generics via inline functions and static member constraints. Example: a generic magnitude function:
\nlet inline magnitude (x: ^a) (y: ^a) : ^a =\n sqrt (x * x + y * y)\n\n// Works with any measured type supporting * and +\nlet hypotenuse = magnitude 3.0<m> 4.0<m> // float<m> = 5.0<m>\nThe inline keyword enables compile-time resolution of the arithmetic operators for the specific measure.
Limitations and Cross-Language Boundaries
\n- \n
- Runtime erasure: Reflection returns base types (
System.Double). Debuggers and serializers see raw numbers. \n - C#/VB consumers: Exposed APIs show only
double,float,decimal. Document expected units in XML comments. \n - No runtime validation: Parsing external data (JSON, databases) requires manual unit assignment after parsing raw numbers. \n
Recovery from Unit Mismatch Errors
\n- \n
- Read the compiler error—it names the expected and actual measures. \n
- Locate the call site. Check whether a constant needs annotation (e.g.,
0.5<m>) or a conversion function is missing. \n - Add the appropriate conversion or annotation. Recompile. \n
- If the mismatch stems from external data, parse to raw numeric first, then apply the measure:
let parsed = Double.Parse(input) * 1.0<m>. \n
Practical Verification Checklist
\n- \n
dotnet buildsucceeds with zero unit-related errors \n- Runtime output matches expected numeric results \n
- No
floatliterals appear unannotated in measured expressions \n - Public APIs include XML comments specifying expected units for non-F# consumers \n
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.