4

I found a nice article: http://nut-cracker.azurewebsites.net/blog/2011/08/09/operator-overloading/

and decided to play a bit with F# using the information presented in the article. I created my own type Vector2D<'a>, supporting addition and multiplication by a constant:

type Vector2D<'a> = Vector2D of 'a * 'a
    with
        member this.X = let (Vector2D (x,_)) = this in x
        member this.Y = let (Vector2D (_,y)) = this in y
        static member inline (+) (Vector2D (lhsx, lhsy), Vector2D (rhsx, rhsy)) =
            Vector2D(lhsx + rhsx, lhsy + rhsy)
        static member inline ( * ) (factor, Vector2D (x, y)) =
            Vector2D (factor * x, factor * y)
        static member inline ( * ) (Vector2D (x, y), factor) =
            Vector2D (factor * x, factor * y)

That works fine - I haven't observed any problems while using these members. Then I decided to add support for dividing by a constant as well (with intend to implement e.g. normalizing vectors):

    static member inline (/) (Vector2D (x, y), factor) =
        Vector2D (x / factor, y / factor)

The thing is even though it compiles fine, every time I try to use it, I see an error:

let inline doSomething (v : Vector2D<_>) =
    let l = LanguagePrimitives.GenericOne
    v / l // error

The error message is:

A type parameter is missing a constraint 'when ( ^a or ^?18259) : (static member ( / ) : ^a * ^?18259 -> ^?18260)'

Moreover - the doSomething's infered type is: Vector2D<'a> -> Vector2D<'c> (requires member (/) and member (/) and member get_One)

My question is: why it's possible to use the members (*) and (+), but it's impossible to do so with (/) even though their defined in the same way ? Also, why the infered type says it needs (/) member twice ?

LA.27
  • 1,888
  • 19
  • 35
  • 1
    You're on to something, and I think I've seen it before. Observations: IntelliSense went into an endless loop when experimenting with this and, even before, didn't predict the error. Removing the first definition of `(*)` on the vector causes the same error with multiplication. In `doSomething`, the type of `l` is underdefined, however, annotating it to be the component type of `v` does not remove the problem. I've once run into similar confusion when trying to use generic vectors to infer matrix logic. I gave up because I couldn't make sense of what the IDE and compiler were doing anymore. – Vandroiy Mar 01 '15 at 15:32
  • 1
    Small comment - you want to put spaces in `(*)` otherwise they can be interperted as starting/ending comments. – John Palmer Mar 01 '15 at 23:28

1 Answers1

3

It says requires / twice because the types are not necessary the same, so your function is going to be more generic than you expect.

When designing generic math libraries some decisions must be taken. You can't just go ahead without type annotating everything or restricting math operators, type inference will be unable to come up with the types.

The problem is that F# arithmetic operators are not restricted, they have an 'open' signature: 'a->'b->'c so either you restrict them or you will have to full type annotate all your functions because if you don't F# will be unable to know which overload should take.

If you ask me I will take the first approach (which is also the Haskell approach), it's a bit complicated at the beginning but once you have everything set up is quite fast to implement new generic types and its operations.

Explaining this will require another post in that blog (I will someday) but I'll give you a short example:

// math operators restricted to 'a->'a->'a
let inline ( + ) (a:'a) (b:'a) :'a = a + b
let inline ( - ) (a:'a) (b:'a) :'a = a - b
let inline ( * ) (a:'a) (b:'a) :'a = a * b
let inline ( / ) (a:'a) (b:'a) :'a = a / b

type Vector2D<'a> = Vector2D of 'a * 'a
    with
        member this.X = let (Vector2D (x,_)) = this in x
        member this.Y = let (Vector2D (_,y)) = this in y
        static member inline (+) (Vector2D (lhsx, lhsy), Vector2D (rhsx, rhsy)) =
            Vector2D(lhsx + rhsx, lhsy + rhsy)
        static member inline (*) (Vector2D (x1, y1), Vector2D (x2, y2)) =
            Vector2D (x1 * x2, y1 * y2)
        static member inline (/) (Vector2D (x1, y1), Vector2D (x2, y2)) =
            Vector2D (x1 / x2, y1 / y2)
        static member inline get_One () :Vector2D<'N> =
            let one:'N = LanguagePrimitives.GenericOne
            Vector2D (one, one)

let inline doSomething1 (v : Vector2D<_>) = v * LanguagePrimitives.GenericOne
let inline doSomething2 (v : Vector2D<_>) = v / LanguagePrimitives.GenericOne

So the idea is not to define all overloads involving your type with numeric types, instead define conversions from numeric types to your type and only the operations between your type.

Now if you want to multiply your vector with another vector or an integer you use the same overload in both cases:

let inline multByTwo (vector:Vector2D<_>) =
    let one = LanguagePrimitives.GenericOne
    let two = one + one
    vector * two  // picks up the same overload as for vector * vector

Of course now you get into the problem of generating generic numbers which is another topic closely related. There are many (different) answers here at SO regarding how to generate a NumericLiteralG module. F#+ is a library that provides an implementation of such module, so you can write vector * 10G

You may wonder why F# operators are not restricted as is the case in other languages like Haskell. This is because some existing .NET types are already defined this way, a simple example is DateTime which support addition of an integer which is arbitrary decided to represent a day, see more discussion here.

UPDATE

To answer your follow up question about how to multiply by floating point number, again this is a whole topic: How to create generic numbers with many possible solutions depending on how generic and scalable your library you want to be. Anyway here is a simple way to get some of that functionality:

let inline convert (x:'T) :'U = ((^T or ^U): (static member op_Explicit : ^T -> ^U) x)

let inline fromNumber x =  // you can add this function as a method of Vector2D
    let d = convert x
    Vector2D (d, d) 

let result1 = Vector2D (2.0f , 4.5f ) * fromNumber 1.5M
let result2 = Vector2D (2.0  , 4.5  ) * fromNumber 1.5M

F#+ does something like this but with rational numbers instead of decimal in order to preserve more precision.

Community
  • 1
  • 1
Gus
  • 25,839
  • 2
  • 51
  • 76
  • Wow - that looks impressive ! Now I see what my mistake was - I simply wasn't aware the functions were too generic. Thanks for making this clear. I understand the way operations (multiplications, etc.) work. Could you just please explain to me how to actually implement multiplication by a floating point number (e.g. 1.5) ? Is there a better way that writting it as a fraction, multiplying by numerator and dividing by denominator ? – LA.27 Mar 01 '15 at 21:58
  • 1
    @AlojzyLeszcz That's a big topic, but see the update for an easy way. – Gus Mar 01 '15 at 22:34
  • Excellent - I feel much wiser now ! :D. Thank you very much! – LA.27 Mar 01 '15 at 23:37
  • @Gustavo Wouldn't it be safer to define the restricted operators on a type, and not globally? The latter wreaks havoc at least when in FSI. I'm thinking of the time-honoured approach e.g. `type Foo = Foo with static member inline (|+) (Foo, (a : 'a, b : 'a)) : 'a = a + b`, and at the calling site `static member inline (+) (Vector2D (lhsx, lhsy), Vector2D (rhsx, rhsy)) = Vector2D(Foo |+ (lhsx, rhsx), Foo |+ (lhsy, rhsy))` which compiles to the same constraint and signature. – kaefer Mar 02 '15 at 08:11
  • @kaefer Yes I did a quick example, but I agree, don't think it's a good idea to have them always restricted globally since you may want to interact with existing .NET code that might have different definitions. One option similar to what you suggest which I find useful is to define then in a module RestrictedOps and then you decide when to bring them in scope. As you said you open it before defining the static members then they are automatically restricted but you can open them at anytime in your client code as well. Another option is to use different names, as the one you wrote for example. – Gus Mar 02 '15 at 08:26