9. Units of Measure
F# supports static checking of units of measure. Units of measure, or measures for short, are like types in that they can appear as parameters to other types and values (as in float<kg>
, vector<m/s>
, add<m>
), can be represented by variables (as in float<'U>
), and are checked for consistency by the type-checker.
However, measures differ from types in several important ways:
- Measures play no role at runtime; in fact, they are erased.
- Measures obey special rules of equivalence , so that
N m
can be interchanged withm N
. - Measures are supported by special syntax.
The syntax of constants (§4.3) is extended to support numeric constants with units of measure. The syntax of types is extended with measure type annotations.
measure-literal-atom :=
long-ident -- named measure e.g. kg
( measure-literal-simp ) -- parenthesized measure, such as (N m)
measure-literal-power :=
measure-literal-atom
measure-literal-atom ^ int32 -- power of measure, such as m^3
measure-literal-seq :=
measure-literal-power
measure-literal-power measure-literal-seq
measure-literal-simp :=
measure-literal-seq -- implicit product, such as m s^- 2
measure-literal-simp * measure-literal-simp -- product, such as m * s^3
measure-literal-simp / measure-literal-simp -- quotient, such as m/s^2
/ measure-literal-simp -- reciprocal, such as /s
1 -- dimensionless
measure-literal :=
_ -- anonymous measure
measure-literal-simp -- simple measure, such as N m
const :=
...
byte < measure-literal > -- 8 - bit unsigned integer constant
uint16 < measure-literal > -- 16 - bit unsigned integer constant
uint32 < measure-literal > -- 32 - bit unsigned integer constant
uint64 < measure-literal > -- 64 - bit unsigned integer constant
sbyte < measure-literal > -- 8 - bit integer constant
int16 < measure-literal > -- 16 - bit integer constant
int32 < measure-literal > -- 32 - bit integer constant
int64 < measure-literal > -- 64 - bit integer constant
ieee32 < measure-literal > -- single-precision float32 constant
ieee64 < measure-literal > -- double-precision float constant
decimal < measure-literal > -- decimal constant
measure-atom :=
typar -- variable measure, such as 'U
long-ident -- named measure, such as kg
( measure-simp ) -- parenthesized measure, such as (N m)
measure-power :=
measure-atom
measure-atom ^ int32 -- power of measure, such as m^3
measure-seq :=
measure-power
measure-power measure-seq
measure-simp :=
measure-seq -- implicit product, such as 'U 'V^3
measure-simp * measure-simp -- product, such as 'U * 'V
measure-simp / measure-simp -- quotient, such as 'U / 'V
/ measure-simp -- reciprocal, such as /'U
1 -- dimensionless measure (no units)
measure :=
_ -- anonymous measure
measure-simp -- simple measure, such as 'U 'V
Measure definitions use the special Measure
attribute on type definitions. Measure parameters, meanwhile, use a variation on the syntax of generic parameters (see §9.5) to parameterize types and members by units of measure. The primitive types byte
, uint16
, uint
, uint64
, sbyte
, int16
, int
, int64
, float
, float32
, decimal
, unativeint
, and nativeint
have non-parameterized (dimensionless) and parameterized versions.
Here is a simple example:
[<Measure>] type m // base measure: meters
[<Measure>] type s // base measure: seconds
[<Measure>] type sqm = m^2 // derived measure: square meters
let areaOfTriangle (baseLength:float<m>, height:float<m>) : float<sqm> =
baseLength*height/2.0
let distanceTravelled (speed:float<m/s>, time:float<s>) : float<m> = speed*time
As with ordinary types, F# can infer that functions are generic in their units. For example, consider the following function definitions:
let sqr (x:float<_>) = x * x
let sumOfSquares x y = sqr x + sqr y
The inferred types are:
val sqr : float<'u> -> float<'u ^ 2>
val sumOfSquares : float<'u> -> float<'u> -> float<'u ^ 2>
Measures are type-like annotations such as kg
or m/s
or m^2
. Their special syntax includes the use of *
and /
for product and quotient of measures, juxtaposition as shorthand for product, and ^
for integer powers.
9.1 Measures
Measures are built from:
- Atomic measures from long identifiers such as
SI.kg
orFreedomUnits.feet
. - Product measures , which are written
measure measure
(juxtaposition) ormeasure * measure
. - Quotient measures , which are written
measure / measure
. - Integer powers of measures , which are written
measure ^ int
. - Dimensionless measures , which are written
1
. - Variable measures, which are written
'u
or'U
. Variable measures can include anonymous measures_
, which indicates that the compiler can infer the measure from the context.
Dimensionless measures indicate “without units,” but are rarely needed, because non-parameterized types such as float
are aliases for the parameterized type with 1
as parameter, that is, float = float<1>
.
The precedence of operations involving measure is similar to that for floating-point expressions:
- Products and quotients (
*
and/
) have the same precedence, and associate to the left, but juxtaposition has higher syntactic precedence than both*
and/
. - Integer powers (
^
) have higher precedence than juxtaposition. - The
/
symbol can also be used as a unary reciprocal operator.
9.2 Constants Annotated by Measures
A floating-point constant can be annotated with its measure by specifying a literal measure in angle brackets following the constant.
Measure annotations on constants may not include measure variables.
Here are some examples of annotated constants:
let earthGravity = 9.81f<m/s^2>
let atmosphere = 101325.0<N m^-2>
let zero = 0.0f<_>
Constants that are annotated with units of measure are assigned a corresponding numeric type with the measure parameter that is specified in the annotation. In the example above, earthGravity
is assigned the type float32<m/s^2>
, atmosphere
is assigned the type float<N/m^2>
and zero
is assigned the type float<'U>
.
9.3 Relations of Measures
After measures are parsed and checked, they are maintained in the following normalized form:
measure-int := 1 | long-ident | measure-par | measure-int measure-int | / measure-int
Powers of measures are expanded. For example, kg^3
is equivalent to kg
kg
kg
.
Two measures are indistinguishable if they can be made equivalent by repeated application of the following rules:
- Commutativity.
measure-int1 measure-int2
is equivalent tomeasure-int2 measure-int1
. - Associativity. It does not matter what grouping is used for juxtaposition (product) of measures, so parentheses are not required. For example,
kg m s
can be split as the product ofkg m
ands
, or as the product ofkg
andm s
. - Identity. 1
measure-int
is equivalent tomeasure-int
. - Inverses.
measure-int / measure-int
is equivalent to1
. - Abbreviation.
long-ident
is equivalent tomeasure
if a measure abbreviation of the form[<Measure>] type long-ident = measure
is currently in scope.
Note that these are the laws of Abelian groups together with expansion of abbreviations.
For example, kg m / s^2
is the same as m kg / s^2
.
For presentation purposes (for example, in error messages), measures are presented in the normalized form that appears at the beginning of this section, but with the following restrictions:
- Powers are positive and greater than 1. This splits the measure into positive powers and negative powers, separated by
/
. - Atomic measures are ordered as follows: measure parameters first, ordered alphabetically, followed by measure identifiers, ordered alphabetically.
For example, the measure expression m^1 kg s^-1
would be normalized to kg m / s
.
This normalized form provides a convenient way to check the equality of measures: given two measure expressions measure-int1
and measure-int2
, reduce each to normalized form by using the rules of commutativity, associativity, identity, inverses, and abbreviation, and then compare the syntax.
To check the equality of two measures, abbreviations are expanded to compare their normalized forms. However, abbreviations are not expanded for presentation. For example, consider the following definitions:
[<Measure>] type a
[<Measure>] type b = a * a
let x = 1<b> / 1<a>
The inferred type is presented as int<b/a>
, not int<a>
. If a measure is equivalent to 1
, however, abbreviations are expanded to cancel each other and are presented without units:
let y = 1<b> / 1<a a> // val y : int = 1
9.3.1 Constraint Solving
The mechanism described in §14.5 is extended to support equational constraints between measure expressions. Such expressions arise from equations between parameterized types — that is, when type<tyarg11 , ..., tyarg1n> = type<tyarg21, ..., tyarg2n>
is reduced to a series of constraints tyarg1i = tyarg2i
. For the arguments that are measures, rather than types, the rules listed in §9.3 are applied to obtain primitive equations of the form 'U = measure-int
where 'U
is a measure variable and measure-int
is a measure expression in internal form. The variable 'U
is then replaced by measure-int
wherever else it occurs. For example, the equation float<m^2/s^2> = float<'U^2>
would be reduced to the constraint m^2/s^2 = 'U^2
, which would be further reduced to the primitive equation 'U = m/s
.
If constraints cannot be solved, a type error occurs. For example, the following expression
fun (x : float<m^2>, y : float<s>) -> x + y
would eventually result in the constraint m^2 = s
, which cannot be solved, indicating a type error.
9.3.2 Generalization of Measure Variables
Analogous to the process of generalization of type variables described in §14.6.7, a generalization procedure produces measure variables over which a value, function, or member can be generalized.
9.4 Measure Definitions
Measure definitions define new named units of measure by using the same syntax as for type definitions, with the addition of the Measure
attribute. For example:
[<Measure>] type kg
[<Measure>] type m
[<Measure>] type s
[<Measure>] type N = kg / m s^2
A primitive measure abbreviation defines a fresh, named measure that is distinct from other measures. Measure abbreviations, like type abbreviations, define new names for existing measures. Also like type abbreviations, repeatedly eliminating measure abbreviations in favor of their equivalent measures must not result in infinite measure expressions. For example, the following is not a valid measure definition because it results in the infinite squaring of X
:
[<Measure>] type X = X^2
Measure definitions and abbreviations may not have type or measure parameters.
9.5 Measure Parameter Definitions
Measure parameter definitions can appear wherever ordinary type parameter definitions can (see §5.2.9). If an explicit parameter definition is used, the parameter name is prefixed by the special Measure
attribute. For example:
val sqr<[<Measure>] 'U> : float<'U> -> float<'U^2>
type Vector<[<Measure>] 'U> =
{ X: float<'U>;
Y: float<'U>;
Z: float<'U> }
type Sphere<[<Measure>] 'U> =
{ Center:Vector<'U>;
Radius:float<'U> }
type Disc<[<Measure>] 'U> =
{ Center:Vector<'U>;
Radius:float<'U>;
Norm:Vector<1> }
type SceneObject<[<Measure>] 'U> =
| Sphere of Sphere<'U>
| Disc of Disc<'U>
Internally, the type checker distinguishes between type parameters and measure parameters by assigning one of two sorts (Type or Measure) to each parameter. This technique is used to check the actual arguments to types and other parameterized definitions. The type checker rejects ill-formed types such as float<int>
and IEnumerable<m/s>
.
9.6 Measure Parameter Erasure
In contrast to type parameters on generic types, measure parameters are not exposed in the metadata that the runtime interprets; instead, measures are erased. Erasure has several consequences:
- Casting is with respect to erased types.
- Method application resolution (see §14.4) is with respect to erased types.
- Reflection is with respect to erased types.
9.7 Type Definitions with Measures in the F# Core Library
The F# core library defines the following types:
type float<[<Measure>] 'U>
type float32<[<Measure>] 'U>
type decimal<[<Measure>] 'U>
type sbyte<[<Measure>] 'U>
type int16<[<Measure>] 'U>
type int<[<Measure>] 'U>
type int64<[<Measure>] 'U>
type nativeint<[<Measure>] 'U>
type uint<[<Measure>] 'U>
type byte<[<Measure>] 'U>
type uint16<[<Measure>] 'U>
type uint64<[<Measure>] 'U>
type unativeint<[<Measure>] 'U>
Note: These definitions are called measure-annotated base types and are marked with the
MeasureAnnotatedAbbreviation
attribute in the implementation of the library. TheMeasureAnnotatedAbbreviation
attribute is not for use in user code and in future revisions of the language may result in a warning or error.
These type definitions have the following special properties:
- They extend
System.ValueType
. - They explicitly implement
System.IFormattable
,System.IComparable
,System.IConvertible
, and corresponding generic interfaces, instantiated at the given type—for example,System.IComparable<float<'u>>
andSystem.IEquatable<float<'u>>
(so that you can invoke, for example,CompareTo
after an explicit upcast). - As a result of erasure, their compiled form is the corresponding primitive type.
-
For the purposes of constraint solving and other logical operations on types, a type equivalence holds between the unparameterized primitive type and the corresponding measured type definition that is instantiated at
<1>
:fsother sbyte = sbyte<1> int16 = int16<1> int = int<1> int64 = int64<1> byte = byte<1> uint16 = uint16<1> uint = uint<1> uint64 = uint64<1> float = float<1> float32 = float32<1> decimal = decimal<1>
-
The measured type definitions
byte
,uint16
,uint
,uint64
,sbyte
,int16
,int
,int64
,float
,float32
,decimal
,unativeint
, andnativeint
are assumed to have additional static members that have the measure types that are listed in the table. Note thatN
is any of these types, andF
is eitherfloat32
orfloat
.
Member | Measure Type |
---|---|
Sqrt |
F<'U^2> -> F<'U> |
Atan2 |
F<'U> -> F<'U> -> F<1> |
op_Addition op_Subtraction op_Modulus |
N<'U> -> N<'U> -> N<'U> |
op_Multiply |
N<'U> -> N<'V> -> N<'U 'V> |
op_Division |
N<'U> -> N<'V> -> N<'U/'V> |
Abs op_UnaryNegation op_UnaryPlus |
N<'U> -> N<'U> |
Sign |
N<'U> -> int |
This mechanism is used to support units of measure in the following math functions of the F# library:
(+)
, (-)
, (*)
, (/)
, (%)
, (~+)
, (~-)
, abs
, sign
, atan2
and sqrt
.
Additionally, the F# core library provides the following measure-annotated aliases, which are functionally equivalent to the previously-listed measure-annotated types, and which are included for the sake of completeness:
type double<[<Measure>] 'U> // aliases float<'U>
type single<[<Measure>] 'U> // aliases float32<'U>
type int8<[<Measure>] 'U> // aliases sbyte<'U>
type int32<[<Measure>] 'U> // aliases int<'U>
type uint8<[<Measure>] 'U> // aliases byte<'U>
type uint32<[<Measure>] 'U> // aliases uint<'U>
9.8 Restrictions
Measures can be used in range expressions but a properly measured step is required. For example, these are not allowed:
[<Measure>] type s
[1<s> .. 5<s>] // error: The type 'int<s>' does not match the type 'int'
[1<s> .. 1 .. 5<s>] // error: The type 'int<s>' does not match the type 'int'
However, the following range expression is valid:
[1<s> .. 1<s> .. 5<s>] // int<s> list = [1; 2; 3; 4; 5]