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).
:?
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).
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With