Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Arithmetic casting to generic type in F#

I try to write a function which does a generic casting for arithmetic types, for example a function which receives an argument of type uint64, then converts to a type being the same as the type parameter. My idea is:

let convert<'T> (x:uint64) = 'T x

But this code does not compile, and I stuck here after trying several approaches like:

let convert<'T> (x:uint64) = 
  match Unchecked.defaultof<'T> with 
    | :? uint32 -> uint32 x
....

So how could I write such a generic arithmetic casting in F#? (I just start learning so my question is maybe stupid, please take it easy).

like image 317
Ta Thanh Dinh Avatar asked Apr 21 '16 23:04

Ta Thanh Dinh


3 Answers

:? type checks only allow (at least that is my understanding) testing for subtypes of the type of the expression that you match on. As 'T can be any type, the compiler can't tell if uint32 is a subtype of that, so that type test is not possible.

To check for "arbitrary" types in match expressions, you need to box the value first, essentially cast it to obj. As all other types are subtypes of obj (Object in C# and the CLR at large), you can then test for whatever types you want.

As you noticed correctly, that alone is not enough, because all branches of the match expression need to return the same type. Because the only common supertype of all number types (that I know of) is again obj, you need to box each conversion again, and then downcast the result of the match to 'T. In theory, that is not 100% type safe, but in this case you know that the conversion will hold.

let convert<'T> (x:uint64) = 
  match box Unchecked.defaultof<'T> with 
    | :? uint32 -> uint32 x |> box
    | :? int -> int x |> box
    :?> 'T

Oh, and it probably wouldn't be a good idea to use something like this in performance critical real world code (tight loops etc., large numbers of calls), because number types are value types allocated on the stack, while each boxing of a number allocates an object on the heap that will have to be garbage collected (iirc, boxing a 4-byte integer creates a 16-byte object, so the difference is quite substantial).

like image 189
TeaDrivenDev Avatar answered Oct 26 '22 23:10

TeaDrivenDev


I'm not claiming this is a good idea, but if you want to take @TeaDrivenDev 's idea one step further you can get around the return type limitation using the generic unbox<'T> method.

The performance overhead on all of this might be significant of course...

Example code:

let convert<'T> (x:uint64) : 'T =
  match box Unchecked.defaultof<'T> with
  | :? uint32 -> uint32 x |> unbox<'T>
  | :? uint16 -> uint16 x |> unbox<'T>
  | :? string -> string x |> unbox<'T>
  | _ -> failwith "I give up"


1u + (12 |> uint64 |> convert) // val it : uint32 = 13u
1us + (uint64 22 |> convert)   // val it : uint16 = 23us

This gets around the limitation of all branches having to return the same type, as each branch does return the same type for any specific generic parameter. The fact that only one branch will ever return for a specific parameter is neither here nor there to the compiler.

like image 39
mavnn Avatar answered Oct 27 '22 00:10

mavnn


You can use static member constraints, here's a "short" example:

type Explicit =
    static member inline ($) (_:byte , _:Explicit) = byte            
    static member inline ($) (_:sbyte, _:Explicit) = sbyte           
    static member inline ($) (_:int16, _:Explicit) = int16           
    static member inline ($) (_:int32, _:Explicit) = int
    // more overloads

let inline convert value: 'T = 
    (Unchecked.defaultof<'T> $ Unchecked.defaultof<Explicit>) value

// Specialized to uint64
let inline fromUint64 (value: uint64) :'T = convert value

// Usage
let x:int = fromUint64 7UL

As said in the comments you can use the function explicit from F#+ which covers all cases when there is an explicit operator. Here's a sample code.

Now if you look at the source code of that function which is defined in another project (FsControl) you will find an even more complicated workaround.

You may wonder why, so here's the long answer:

In theory it should be possible to use a single call invoking the member op_Explicit, but that would only work when that member really exists which is not the case with the native number types.

For those cases the F# compiler uses a feature usually referred to as "simulated members" which is implemented using static optimizations but that's not available outside the F# compiler source code.

So F#+ uses a different feature instead: overload resolution, as in the sample code I showed you but it uses an additional overload as a general case for those members which really contain an op_Explicit static member.

like image 2
Gus Avatar answered Oct 27 '22 01:10

Gus