TL;DR How can I write an "overloaded" function that can handle all numeric types with units and a separate type with that same unit (float32<m> -> Vector2<m>, int<kg> -> Vector2<kg>)?
I have a nice little Vector2 class, but I want to allow units of measure to be attached to it, so I've essentially defined it like this (but with a lot more functionality than included here):
type Vector2<[<Measure>] 'u>(x: float32<'u>, y: float32<'u>) =
member this.X = x
member this.Y = y
I've been able to write overloaded arithmetic operators without much difficulty as well (e.g. static member (+) ...), but I've been really struggling to write a constructor operator (called @@) that can work with all numeric types as well as units (allowing me to write 1<m> @@ 2.5<m> instead of new Vector2<m>(1.<m>, 2.<m>). Before I attached units to this class, it was really easy:
let inline (@@) x y = new Vector2(float32 x, float32 y)
So far I've written a helper class to deal with this:
type OverloadedOperators =
static member CreateVector2 (x: float32<'u>, y: float32<'u>) = new Vector2<'u>(x, y)
static member CreateVector2 (x: float<'u>, y: float<'u>) = new Vector2<'u>(x |> float32 |> Float32WithMeasure<'u>, y |> float32 |> Float32WithMeasure<'u>)
static member CreateVector2 (x: int<'u>, y: int<'u>) = new Vector2<'u>(x |> float32 |> Float32WithMeasure<'u>, y |> float32 |> Float32WithMeasure<'u>)
but it seems impossible to write the appropriate type constraint to handle this call case. For example, something like this doesn't work
let inline (@@) (x: 'u when 'u : (static member CreateVector2 : 'u * 'u -> Vector2<'v>)) y = OverloadedOperators.CreateVector2 (x, y)
because The type 'u is not compatible with float32<'v>, The type 'u is not compatible with int<'v>, etc.
I'm not sure how to write this last little piece. Is what I want even possible? Or should I just put up with constantly having to use the Vector2 constructor everywhere?
EDIT I've gotten really close thanks to @JohnPalmer and I've come up with this last little bit, building on his answer: let inline vec2 p = convert *** p. Now the last step would be to "un-tuple" this function to allow it to become an infix operator, but this seems to be impossible since it doesn't even really know its always a tuple in the first place. I think I may have come as far as I can with F# here, but correct me if I'm wrong! In the mean time I'll settle with vec2 (10.<m>, 20.<m>), which is a lot better than having to do all of the messy unit type casts inline every single time I call the constructor.
Here is one slightly hacky solution that builds from the standard trick we use in this sort of case:
open LanguagePrimitives
type Vector2<[<Measure>] 'u>(x: float32<'u>, y: float32<'u>) =
member this.X = x
member this.Y = y
type OverloadedOperators() =
static member CreateVector2 (x: float32<'u>, y: float32<'u>) = new Vector2<'u>(x, y)
static member CreateVector2 (x: float<'u>, y: float<'u>) = new Vector2<'u>(x |> float32 |> Float32WithMeasure<'u>, y |> float32 |> Float32WithMeasure<'u>)
static member CreateVector2 (x: int<'u>, y: int<'u>) = new Vector2<'u>(x |> float32 |> Float32WithMeasure<'u>, y |> float32 |> Float32WithMeasure<'u>)
static member ( *** ) (T,(x:float32<'u>,y)) = OverloadedOperators.CreateVector2(x,y)
static member ( *** ) (T,(x:float<'u>,y)) = OverloadedOperators.CreateVector2(x,y)
static member ( *** ) (T,(x:int<'u>,y)) = OverloadedOperators.CreateVector2(x,y)
let convert =OverloadedOperators()
(* testing *)
[<Measure>]
type m
convert *** (1<m>,1<m>)
convert *** (1.0<m>,1.0<m>)
It uses two operators and is overly verbose, but is reasonably close to what you want
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