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:
- Using overloads (a la C#)
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 ...
- Using inline, a common trick is retyping:
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.
- Using both overloads and inline: this trick will solve both issues by use overloading through an intermediate type, this way you get both compile-time checks and you'll be able to define generic functions:
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)
- Taking advantage of some generic constraints:
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.