The design suggestion Extend Units of Measure to Include Unsigned Integers has been marked "approved in principle".
This RFC covers the detailed proposal for this suggestion.
- Approved in principle
- Suggestion
- Implementation
- Design Review Meeting(s) with @dsyme and others invitees
- Discussion
This feature builds on the existing implementation for Units of Measure, by extending it to support additional numeric types.
Consider the following example:
let weight = 69.275<Kg>
// val weight : float<Kg>
let age = 25<days>
// val age : int<days>
let questionable_age = -3<days>
// val questionable_age : int<days>
(* We'd really like to express `age` as a non-negative integer *)
let better_age = 3u<days>
// -------------^^^^^^^^
// error FS0636: Units-of-measure supported only on float, float32, decimal and signed integer typesWere this RFC to be implemented, the final line above would compile. Thus, allowing correct expression of both the numeric type and the units of measure.
At a minimum, this should include unsigned integers (i.e. types expressing 8-bit, 16-bit, 32-bit, and 64-bit non-negative integer values).
Further, this RFC proposes also extending the support to native integers (both signed and unsigned).
However, in the interest of controlling scope, this RFC only considers "primitive numeric types" (i.e. CLR value types which express numeric quantities... sorry, BigInt).
This work provides the following benefits:
- Increase the overall "safety" by obviating the need to choose between a type or the ability to carry a measure.
- Provide more consistency/uniformity across primitive numeric types.
- Reduce the number of "quirks", or "gotchas!", one encounters when learning Units of Measure.
For clarity's sake, the following table outlines the type abbreviations targeted by this RFC. All others should be considered out-of-scope (i.e. addressed by other RFCs).
| F# alias | CLR Type |
|---|---|
single |
System.Single |
double |
System.Double |
int8 |
System.SByte |
int32 |
System.Int32 |
byte |
System.Byte |
uint8 |
System.Byte |
uint16 |
System.UInt16 |
uint |
System.UInt32 |
uint32 |
System.UInt32 |
uint64 |
System.UIn64 |
nativeint |
System.IntPtr |
unativeint |
System.UIntPtr |
The proposed approach is to simply use the same mechanism currently employed (e.g., for floats).
Specifically, within FSharp.Core, for each new "measure-bearing type" we add an appropriately-annotated alias to an existing numeric type.
For example, here's how a "measure-bearing" float<m> is currently defined (n.b. XMLDocs elided for clarity):
[<MeasureAnnotatedAbbreviation>]
type float<[<Measure>] 'Measure> = floatFor a new type, such as, e.g. uint<m>, the work is largely a copy-paste job:
[<MeasureAnnotatedAbbreviation>]
type uint<[<Measure>] 'Measure> = uintAdditionally, for each new "measure-bearing type", we add a function to the LanguagePrimitives module.
For example, paired with the uint<_> abbreviation given above, we add:
let inline UInt32WithMeasure (f : uint) : uint<'Measure> = retype fWe then also extend the compiler (fsc) in the following ways (again, mostly copying existing values and tweaking them as needed):
- Ensure that each new type can be resolved out of FSharp.Core (i.e. define and expose a
TyconnRefin TcGlobals.fs). - Ensure the previous step is surfaced as TAST objects (in import.fs)
- Make certain the type checker (TypeChecker.fs) considers the new TAST objects when validating measure-annotated code
Obviously, for completeness sake, an implementation of this RFC also needs to adjust various tests, documentation, and error messages as appropriate.
Main drawback? Somebody has to do the work. It also represents a slight increase in the "surface area" of the language, which means more code to support and maintain.
For the specific scope of this RFC, no other designs have been considered, as the obvious solution (of using what's already there) seems sufficient. However, there are other language suggestions which advocate Units of Measure being evolved into a more general-purpose "type tagging" mechanism. If any such efforts develop, it's quite likely the current implement (as detailed in this RFC) would need to be revised.
Further, if we simple choose not to do this work, then we're no worse off than the current production version of F#.
Please address all necessary compatibility questions:
- Is this a breaking change? No. However, current code which uses hacks to simulate this feature (c.f. here or here) may encounter issues.
- What happens when previous versions of the F# compiler encounter this design addition as source code? They will treat it as an invalid construct.
- What happens when previous versions of the F# compiler encounter this design addition in compiled binaries? They will treat it as an invalid construct.
- If this is a change or extension to FSharp.Core, what happens when previous versions of the F# compiler encounter this construct? They will ignore it.
(n.b. several of the above answers are "well-educated guesses", which need proper vetting.)
None.