Aug 092011
 

In my previous post we defined an overloaded AddVector function.
There is a more convenient way to define those functions: using operators.
We have basically two ways of defining operators in F#, one alternative is to define them at the global scope level as any other function.
Another way is to define them as static members of a type.

Consider this code:

type Vector2D = Vector2D of float * float with
    static member Add(Vector2D(a1,a2),Vector2D(b1,b2)) = Vector2D(a1+b1, a2+b2)

type Vector3D = Vector3D of float * float * float with
    static member Add(Vector3D(a1,a2,a3), Vector3D(b1,b2,b3)) = Vector3D(a1+b1, a2+b2, a3+b3)

We have only the function Add defined for Vector2D and Vector3D, but those are different functions, so if we invoke them we have to specify the Vector type, I mean if we try this:

Add(Vector2D (2.0,3.0) , Vector2D (5.0,3.0) )

We get error FS0039: The value or constructor ‘Add’ is not defined.
If we specify the Class it will work.

Vector2D.Add(Vector2D (2.0,3.0) , Vector2D (5.0,3.0) )

To avoid specifying the class, in the last post we defined an AddVectors which will pick up the right function.
But we could have defined operators:

type Vector2D = Vector2D of float * float with
    static member (+.)  (Vector2D(a1,a2),Vector2D(b1,b2)) = Vector2D(a1+b1, a2+b2)
    static member (~-.) (Vector2D(a1,a2)) = Vector2D (-a1 , -a2)

type Vector3D = Vector3D of float * float * float with
    static member (+.)  (Vector3D(a1,a2,a3), Vector3D(b1,b2,b3)) = Vector3D(a1+b1, a2+b2, a3+b3)
    static member (~-.) (Vector3D(a1,a2,a3)) = Vector3D (-a1, -a2, -a3)

This way we can write directly Vector2D (1.0,2.0) +. Vector2D (6.0,5.0) and it will pick up the right function.
And instead of SubVectors we can define the binary (-.) operator, re-using previous operators:
Now let’s suppose for compatibility reasons we want to define AddVectors, InvVector and SubVectors

let inline AddVectors x y = x +. y
let inline InvVector  x   = -.x
let inline SubVectors x y = x +. -.y

Here we can see how it works, this is the type inferred for AddVectors.

val inline AddVectors :
   ^a ->  ^b ->  ^_arg3
    when ( ^a or  ^b) : (static member ( +. ) :  ^a *  ^b ->  ^_arg3)

When we write exp1 +. exp2 the compiler tries to find the operator defined at the global scope, then as a static member in the type of exp1 and in the type of exp2.

We could have defined all this code with (+) and (-) but then of course, these functions will work with everything that has a binary (+) operation and an unary (-) operation defined, I mean if we call SubVectors 10.0 5.0 will work as well.

Now we will try to generalize this code.
Right now it works only with floats but let’s make it work with all underlying types that support (+) and (-).
All we have to do is make the static members inline, type inference will do the rest for us.

type Vector2D<'a> = Vector2D of 'a * 'a with
    static member inline ( +.) (Vector2D(a1,a2),Vector2D(b1,b2)) = Vector2D(a1+b1, a2+b2)
    static member inline (~-.) (Vector2D(a1,a2)) = Vector2D (-a1 , -a2)

type Vector3D<'a> = Vector3D of 'a * 'a * 'a with
    static member inline ( +.) (Vector3D(a1,a2,a3), Vector3D(b1,b2,b3)) = Vector3D(a1+b1, a2+b2, a3+b3)
    static member inline (~-.) (Vector3D(a1,a2,a3)) = Vector3D (-a1, -a2, -a3)

let inline AddVectors x y = x +. y
let inline InvVector  x   = -.x
let inline SubVectors x y = x +. -.y

Global Level operators vs operators as static members

The global level definition in fact is not that global, it is restricted to the scope.
Surprisingly, it has priority over the static member definition.
When an operator is found the compiler first try to find a “global” definition at the local scope, if no definition is found then it is assumed as an operator defined at the type of one of the operands.

In F# some operators are already defined at the global level, for instance (+) operator is defined at global level, then in that definition the plus operator as static member is invoked.

Let’s compare these definitions at global level:

> let inline f x = (+) x ;;

val inline f :
   ^a -> ( ^b ->  ^c)
    when ( ^a or  ^b) : (static member ( + ) :  ^a *  ^b ->  ^c)

> let inline g x = ($) x ;;

val inline g :
   ^a -> ( ^_arg2 ->  ^_arg3)
    when ( ^a or  ^_arg2) : (static member ( $ ) :  ^a *  ^_arg2 ->  ^_arg3)


Function f has a different signature as g. This is because f is re-using the global definition from (+) while in the other case there is no global definition for ($), so function g will call directly a static member ($) in the first or the second operand type.
The constraints we have in function f are automatically generated by the use of (+) in his body, taken from (+) definition at global definition whereas the constraints of g are generated “from scratch”.

Conclusion
If used properly operators can improve code readability.
Operators can behave as functions, we just need to enclose them in parenthesis.
There is no need to specify static constraints, in most cases they are automatically guessed by the compiler.
Another possibility is to define operators at the global scope.

 Leave a Reply

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

(required)

(required)