Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

f# generic units over generic types

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.

like image 464
Bram Avatar asked Jan 05 '13 04:01

Bram


1 Answers

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:

  1. 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 ...

  1. 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.


  1. 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)
  1. 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.

like image 104
Gus Avatar answered Sep 18 '22 05:09

Gus