Why Units of Measure Exist
Units of measure (UoM) let you attach a compile-time-only tag like <meter>, <second>, or <kg> to a numeric type, so 3.0<meter> and 3.0<second> are both float values under the hood but are distinct, incompatible types to the compiler. This directly targets the class of bug where a value's numeric type-checks perfectly (it's just a float) but its physical meaning is wrong -- famously, NASA's Mars Climate Orbiter was lost in 1999 because one team's software produced thrust data in pound-force-seconds while another expected newton-seconds, and nothing in either codebase's types caught the mismatch.
Cricket analogy: Units of measure are like distinguishing a bowling figure in overs from one in balls bowled -- both are just numbers, but mixing up '10 overs' with '10 balls' in a run-rate calculation gives a nonsensical result, the same category of silent bug UoM prevents by tagging 10.0<over> differently from 10.0<ball>.
Declaring and Attaching Units
You declare a unit with [<Measure>] type m (for meters) and [<Measure>] type s (for seconds), then attach them to literals with angle-bracket syntax: let distance = 100.0<m> and let time = 9.58<s>. Units compose: dividing 100.0<m> / 9.58<s> produces a value of type float<m/s> automatically, and the compiler derives compound units like m/s or m/s^2 from arithmetic rather than requiring you to declare them separately, though you can also declare a named compound unit like [<Measure>] type N = kg * m / s^2 for readability.
Cricket analogy: Declaring [<Measure>] type over and attaching it as 10.0<over> is like the scoring convention that treats overs as a distinct unit from balls in official records; compound derivation, like runs-per-over, mirrors how 100.0<run> / 10.0<over> yields a run/over type automatically without the compiler needing a separate 'strike rate unit' declared upfront.
[<Measure>] type m
[<Measure>] type s
let distance = 100.0<m>
let time = 9.58<s>
let speed = distance / time // float<m/s>
[<Measure>] type kg
[<Measure>] type N = kg * m / s^2
let force (mass: float<kg>) (accel: float<m/s^2>) : float<N> =
mass * accelArithmetic and Unit Inference
Addition and subtraction require identical units -- 5.0<m> + 3.0<m> type-checks and yields 8.0<m>, but 5.0<m> + 3.0<s> is a compile error -- while multiplication and division freely combine or cancel units, so 10.0<m> * 3.0<s> produces 30.0<m s> and 10.0<m> / 10.0<m> produces a dimensionless 1.0 (with no unit at all). This means the compiler will reject an accidentally-transposed formula, such as computing kinetic energy as mass * velocity instead of 0.5 * mass * velocity * velocity, purely because the resulting unit doesn't match the <kg m^2/s^2> you declared the function to return.
Cricket analogy: The rule that 5.0<over> + 3.0<over> works but 5.0<over> + 3.0<ball> doesn't mirrors how a scorecard can sum two spells' overs together directly, but can't meaningfully add 'overs bowled' to 'balls faced' without first converting, the same dimensional discipline UoM enforces on addition.
[<Measure>] type ft
let metersToFeet (m: float<m>) : float<ft> =
m * 3.28084<ft/m>
let trackLength = 100.0<m>
let trackLengthInFeet = metersToFeet trackLength // 328.084<ft>
// Stripping the unit back to a plain float:
let raw = trackLength / 1.0<m> // 100.0 : floatYou can write generic functions over units of measure too: let square (x: float<'u>) : float<'u ^ 2> = x * x works for float<m>, float<s>, or any other unit, deriving float<m^2> or float<s^2> automatically at each call site -- units of measure participate in generic type inference just like ordinary type parameters.
Converting Between Units and Stripping Measures
Converting between units, such as meters to feet, requires an explicit conversion factor with its own unit -- let metersToFeet (m: float<m>) : float<ft> = m * 3.28084<ft/m> -- because the compiler has no built-in knowledge that meters and feet measure the same underlying dimension; you must supply the relationship yourself. To strip a unit entirely and get back a plain float, you divide by a quantity of the same unit (5.0<m> / 1.0<m> gives 5.0), and there's no automatic 'unwrap' operation because doing so silently would defeat the entire purpose of tracking units in the first place.
Cricket analogy: Converting metersToFeet with an explicit conversion factor is like converting a bowler's speed reading from kilometers per hour to miles per hour on a broadcast graphic -- the two numbers describe the same physical pace, but the broadcaster must apply a known conversion factor (0.621371) rather than assume the systems interoperate automatically.
Units of measure are entirely erased at runtime -- 3.0<m> compiles down to the exact same IL as a plain 3.0, so there is zero runtime overhead, but also zero runtime checking. If you use obj, reflection, or serialize a value and lose the static type (e.g., round-tripping through JSON as a plain float), nothing prevents you from reattaching the wrong unit on deserialization.
- Units of measure attach a compile-time-only tag like <m> or <s> to numeric types.
- Declared with [<Measure>] type m, then attached to literals as 3.0<m>.
- Addition and subtraction require identical units; multiplication and division freely combine or cancel units.
- Compound units like <m/s> or <N> are derived automatically from arithmetic, or can be named explicitly.
- Converting between units (e.g., meters to feet) requires an explicit, manually supplied conversion factor.
- Units are completely erased at runtime -- no performance cost, but also no runtime validation.
- Units of measure catch physically nonsensical formulas at compile time, like the class of bug that doomed the Mars Climate Orbiter.
Practice what you learned
1. What does 5.0<m> + 3.0<s> produce in F#?
2. What is the result type of 10.0<m> / 10.0<m>?
3. Why does converting meters to feet require writing an explicit conversion function in F#?
4. What happens to units of measure at runtime?
Was this page helpful?
You May Also Like
Generic Types in F#
Explore how F# generic types and automatic type inference let you write reusable code that works across many types safely.
Discriminated Unions
Learn how F#'s discriminated unions model a closed set of named cases, each optionally carrying data, with compiler-enforced exhaustive pattern matching.
Structural Equality
Learn why F#'s default equality compares records, tuples, and unions by value instead of by reference, and when to opt out.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics