Reputation: 628
Is it possible to define a function that is both generic over data type and unit of measure? E.g., what I would like to do, but doesn't compile (though it wouldn't even without units of measure present, but I believe I conveys what I'd like to do):
let inline dropUnit (x : 'a<_>) = x :> typeof(a)
the idea here is that I've defined some units of measure, e.g. "kg" and "l" and a dicriminated union:
type Unit =
| Weight of float< kg >
| Volume of float < l >
and I would like to do something like:
let isValidUnitValue myUnit =
match myUnit with
| Weight(x) -> (dropUnit x) > 0.
| Volume(x) -> (dropUnit x) > 0.
I am aware that for this particular case I could just use
let dropUnit (x : float<_>) = (float) x
but I started wondering about the general case while writing the above.
Upvotes: 6
Views: 563
Reputation: 26174
For your specific question how to write your isValidUnitValue
function, the answer is:
let inline isValidUnitValue myUnit = myUnit > LanguagePrimitives.GenericZero
So you don't need to define a Discriminated Union.
Regarding the original question whether is it possible to define a function that is both generic over data type and unit of measure like dropUnit
the short answer is no. If such function exists it would have a signature like 'a<'b> -> 'a
and in order to represent it the type system should implement higher kinds.
However there are tricks using overload and inline:
type UnitDropper =
static member drop (x:sbyte<_> ) = sbyte x
static member drop (x:int16<_> ) = int16 x
static member drop (x:int<_> ) = int x
static member drop (x:int64<_> ) = int64 x
static member drop (x:decimal<_>) = decimal x
static member drop (x:float32<_>) = float32 x
static member drop (x:float<_> ) = float x
[<Measure>] type m
let x = UnitDropper.drop 2<m> + 3
But this is not really a generic function, you can't write something generic on top of it.
> let inline dropUnitAndAdd3 x = UnitDropper.drop x + 3 ;;
-> error FS0041: A unique overload for method 'drop' could not be determined ...
let inline retype (x:'a) : 'b = (# "" x : 'b #)
[<Measure>] type m
let x = retype 2<m> + 3
let inline dropUnitAndAdd3 x = retype x + 3
The problem is that retype
is too generic, it will allow you write:
let y = retype 2.0<m> + 3
Which compiles but will fail at run-time.
type DropUnit = DropUnit with
static member ($) (DropUnit, x:sbyte<_> ) = sbyte x
static member ($) (DropUnit, x:int16<_> ) = int16 x
static member ($) (DropUnit, x:int<_> ) = int x
static member ($) (DropUnit, x:int64<_> ) = int64 x
static member ($) (DropUnit, x:decimal<_>) = decimal x
static member ($) (DropUnit, x:float32<_>) = float32 x
static member ($) (DropUnit, x:float<_> ) = float x
let inline dropUnit x = DropUnit $ x
[<Measure>] type m
let x = dropUnit 2<m> + 3
let inline dropUnitAndAdd3 x = dropUnit x + 3
let y = dropUnit 2.0<m> + 3 //fails at compile-time
In the last line you'll get a compile-time error: FS0001: The type 'int' does not match the type 'float'
Another advantage of this approach is that you can extend it later with new types by defining a static member ($) in your type definition like this:
type MyNumericType<[<Measure 'U>]> =
...
static member dropUoM (x:MyNumericType<_>) : MyNumericType = ...
static member ($) (DropUnit, x:MyNumericType<_>) = MyNumericType.dropUoM(x)
let inline retype (x: 'T) : 'U = (# "" x: 'U #)
let inline stripUoM (x: '``Num<'M>``) =
let _ = x * (LanguagePrimitives.GenericOne : 'Num)
retype x :'Num
This is similar to 2) but it doesn't require a type annotation. The limitation is that it works for numeric types only, but normally that's the use case with UoMs.
Upvotes: 12