Zero‑Overhead Dimensional Safety in F#: How Units of Measure Prevent Runtime Errors
F#’s Units of Measure let you attach dimensional units to numeric values, catching mistakes like adding meters to seconds at compile time. This post walks through defining units, using them in calculations, and highlights IDE support, trade‑offs, and a quick checklist.
07 Jul 2025, 14:15 UTC

Problem: Mixing Units Leads to Silent Bugs
In many codebases, numeric values are treated as plain numbers. A common pitfall is adding meters to seconds or multiplying unrelated quantities, which compiles fine but produces meaningless results. These errors surface only at runtime, often after deployment.
Thesis: Units of Measure Give You Compile‑Time Dimensional Safety
F#’s Units of Measure attach dimensional information to numeric types. The compiler checks that operations respect these units, catching mismatches before the code runs. Because the unit metadata is erased during compilation, there is zero runtime overhead.
1. What Are Units of Measure?
- Compile‑time annotations that describe the dimensionality of numeric values.
- Implemented via the
[]attribute applied to a type. - Supported on all .NET numeric primitives:
float,int,decimal, etc. - Derived units can be expressed through multiplication, division, and exponentiation.
2. Defining and Using Units in Code
[] type m // meters
[] type s // seconds
let distance = 5.0<m>
let time = 2.0<s>
let speed = distance / time // 2.5<m/s>
printfn "%f" speed
Build and run the code. The output is 2.500000. Notice that the compiler infers the resulting unit m/s and enforces that distance and time have compatible units for division.
Attempting an invalid operation:
let bad = distance + time // ❌ compile‑time error
The compiler rejects this with an error similar to: The unit of measure 's' does not match 'm'. This prevents the silent bug that would otherwise occur.
3. IDE Support and Debugging
- Visual Studio, Rider, and VS Code (Ionide) highlight unit mismatches inline.
- Quick‑fix suggestions can automatically insert missing units or convert between compatible units.
- During debugging, watch windows show the raw numeric value; the unit information is not present at runtime, so rely on compile‑time guarantees.
4. Trade‑offs and Limitations
- Only numeric types can carry units. Strings, arrays, or custom structs cannot be annotated directly.
- Units are erased at runtime, so debugging tools cannot display them post‑compile.
- Interoperability with libraries that do not use units requires careful casting or conversion.
- Unit definitions are global; naming collisions can occur in large solutions.
5. Practical Checklist for Safe Numeric Code
- Define base units with
[]once per domain (e.g.,m,s,kg). - Annotate all numeric constants and variables that represent physical quantities.
- Use derived units for complex formulas (e.g.,
m/s^2for acceleration). - Run the build; any unit mismatch will surface immediately.
- Verify output with a simple
dotnet runtest to ensure calculations produce expected values.
Adopting Units of Measure in your F# projects leads to more robust, self‑documenting code without any performance penalty.
Conclusion
F#’s Units of Measure are a powerful, zero‑overhead tool for preventing dimensional errors. By integrating them into your coding workflow, you trade a small upfront learning curve for long‑term reliability and maintainability.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.