Preventing Numeric Logic Bugs with F# Units of Measure
Learn how to use F# Units of Measure to eliminate silent numeric bugs by encoding physical dimensions into the type system for compile-time safety.
26 Apr 2026, 16:58 UTC

The most dangerous bugs in numeric code are not the ones that crash the program, but the ones that produce silently incorrect results. When a function expects milliseconds but receives seconds, or when a calculation mixes meters and feet without a conversion factor, the program continues to execute—while the business logic fails. In F#, you can solve this using a zero-cost abstraction called Units of Measure. This feature encodes physical dimensions into the type system, catching dimensional mismatches at compile time.
How Units of Measure Work
Units of Measure allow you to annotate numeric types (such as float) with a unit-specific tag. These tags are not runtime objects; they exist solely for the compiler's benefit. When you perform arithmetic, the compiler tracks the units based on dimensional analysis rules.
If you add two values with the same unit, the result retains that unit. If you divide distance by time, the result becomes a new unit representing velocity. If you attempt to add two incompatible units, the compiler rejects the code.
Worked Example: Safe Velocity Calculations
Consider a scenario where you are calculating the speed of a drone. By defining custom measures, you ensure that distance and time scales are never mixed accidentally.
// Define custom measures using attributes
[<Measure>] type Meters
[<Measure>] type Seconds
// The function signature explicitly requires Meters and Seconds
let calculateSpeed (distance: float<Meters>, time: float<Seconds>) : float<Meters/Seconds> =
distance / time
let flightDistance = 500.0<Meters>
let flightTime = 20.0<Seconds>
// This compiles and runs correctly
let speed = calculateSpeed(flightDistance, flightTime)
printfn "Speed is: %f m/s" speed
// This would cause a COMPILE TIME ERROR:
// let wrongTime = 20.0<Meters>
// let badSpeed = calculateSpeed(flightDistance, wrongTime)
In this example, the type float<Meters> is distinct from float<Seconds>. If you try to pass a distance value where a time value is expected, the build fails, preventing the logic error from reaching production.
Handling Domain-Specific Units
Beyond physics, you can model any domain concept. For instance, you can define units for currency, pixels, or request counts to prevent adding "USD" to "EUR" or "Pixels" to "Percentages".
[<Measure>] type USD
[<Measure>] type EUR
let balance = 100.0<USD>
let refund = 20.0<USD>
// Valid: same units
let total = balance + refund
// Invalid: compiler error because USD and EUR are incompatible
// let mixed = balance + 10.0<EUR>
Trade-offs and Limitations
While powerful, Units of Measure have specific constraints:
- Runtime Erasure: Units are erased after compilation. The generated Common Intermediate Language (CIL) uses raw floats. This means when sending data to a database or a JSON API, you must manually strip the unit or handle the conversion to a primitive type.
- Generic Constraints: Units do not flow through arbitrary generic type parameters easily. If you write a generic function
add<'T>, the compiler cannot track the units of'Twithout specialized wrappers orLanguagePrimitives. - Primitive Requirement: This feature applies only to primitive numeric types. You cannot apply a unit of measure to a custom record or a complex object.
Verification and Implementation
To verify this behavior locally, you can use an F# script (.fsx) or a console project. Run the following command to start a project:
dotnet new console -lang "F#"
Add a deliberate unit mismatch (e.g., adding 1.0<Meters> to 1.0<Seconds>) and run dotnet build. The compiler will provide an error specifying that the units are incompatible, confirming that the safety check is active.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.