In principle overloading is just a syntactic construct where we define two or more different functions with the same name, but for this post I would like to point out a distinction between two different overloading approaches:
For the rest of this post I will use the terms “unrelated overload” and “related overload”.
Unrelated overloads: When there are different number of parameters or the same number but still with no intrinsic relationship between each overload parameter types.
We find this ad-hoc overloading in many .Net framework methods like Int32.Parse where we have one overload that use NumberStyles, another that uses IFormatProvider and another one that uses both.
Related overloads: The same number of parameters with different input parameter types but with an intrinsic relationship between them.
As an example, F# defines the operator (*) for several types, but those types are different way of representing numbers.
In languages like C# you can define both related and unrelated overloads but as we will see later it will not allow you to scale up and generalize over overloaded functions.
In Haskell you can’t define unrelated overloads, in order to define overloads you need to establish first a relationship between the different types through typeclasses, but this will allow you to create generic functions on top of the overloaded function.
Another difference is in Haskell you can have two functions where the only overloaded parameter is the output parameter. This makes sense because in FP languages with currying there no distinction between input and output parameters.
Because F# is functional and on top of the .NET framework both types of overload are available in F#, the former happens at .NET compile time and the latter is handled by the F# compiler.
Overloading in F#
The first problem we can imagine when overloading in most functional programming languages is conflicting with currying when we have different number of parameters, but this can be solved if we use tupled arguments.
Anyway if we want to write directly different overloads in F# we have to do it in methods and method’s parameters can’t be directly curried.
The next problem could be type inference.
This approach works the same as in C#, the overloading is resolved at compile time each time we call the method.
Related and unrelated overloading can be achieved this way.
There is another way to do overloading in F# which is not available in C#: inline functions with static constraints.
This option requires, as in Haskell, to specify first a relationship between the types.
In F# you specify this relationship with static constraints.
type Vector2D = Vector2D of float * float with static member Add(Vector2D(a1,a2),Vector2D(b1,b2)) = Vector2D(a1+b1, a2+b2) static member Inv(Vector2D(a1,a2)) = Vector2D (-a1 , -a2) 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) static member Inv(Vector3D(a1,a2,a3)) = Vector3D (-a1, -a2, -a3) let inline AddVectors x y = (^Vector: (static member Add: ^Vector -> ^Vector -> ^Vector) (x,y)) let inline InvVector x = (^Vector: (static member Inv: ^Vector -> ^Vector) (x))
In the code above, the function AddVectors will accept any type that has a static member Add with the signature ‘a->’a->’a.
A function declared inline will be resolved also at compile time at every point that function is called.
This kind of functions can be called also from other F# assemblies, because they are “shipped” with F# metadata which allows inlining in another F# assembly.
AddVectors (Vector2D (1,2)) (Vector2D (3,4)) let Sum3D (x:Vector3D) = AddVectors x
At the caller site should be clear which overload will be used, but not always.
What happens if the caller is inside another inline function?
In that case the caller function is also inline, so in this case the type is still not “decided”.
This way we can define generic functions by “chaining” inline functions.
let inline SubVectors x y = AddVectors x (InvVector y)
Note that here it’s no longer necessary to declare the static constraints, type inference is smart enough to do it for us.
If we compare this technique with C#’s method overloading, what’s the difference? Can’t we achieve the same with standard .Net method overloading?
The first difference is with inline we defined a function that will decide between two methods (which are seen as the same method) on two different types whereas with member overloading the decision is between two different methods on the same type.
Anyway we can rewrite the code like this:
type Vector2D = Vector2D of float * float type Vector3D = Vector3D of float * float * float type VectorOperations = VectorOperations with static member Add(Vector2D(a1,a2),Vector2D(b1,b2)) = Vector2D(a1+b1, a2+b2) static member Inv(Vector2D(a1,a2)) = Vector2D (-a1 , -a2) static member Add(Vector3D(a1,a2,a3), Vector3D(b1,b2,b3)) = Vector3D(a1+b1, a2+b2, a3+b3) static member Inv(Vector3D(a1,a2,a3)) = Vector3D (-a1, -a2, -a3)
And now we can call those functions this way:
VectorOperations.Add (Vector2D (1.0,2.0) ,Vector2D (3.0,4.0)) VectorOperations.Add (Vector3D (1.0,2.0,4.0) ,Vector3D (3.0,4.0,6.0))
What happen here? The method is resolved at the point is called, .Net support this without inlining.
Now the question is, can we go further and chain generic functions?
Here’s the answer
> let SubVectors x y = VectorOperations.Add ( x , (VectorOperations.Inv y)) ;; let SubVectors x y = VectorOperations.Add ( x , (VectorOperations.Inv y)) ;; -------------------------------------------------^^^^^^^^^^^^^^^^^^^^^^ stdin(6,50): error FS0041: A unique overload for method 'Inv' could not be determined based on type information prior to this program point. The available overloads are shown below (or in the Error List window). A type annotation may be needed. Possible overload: 'static member VectorOperations.Inv : Vector3D -> Vector3D'. Possible overload: 'static member VectorOperations.Inv : Vector2D -> Vector2D'.
It doesn’t work because at this point the compiler needs to decide which overload are we using, there’s no way to continue chaining, except defining again two overloaded methods which will contain exactly the same code, but operate on different types
type VectorOperations' = VectorOperations' with static member Sub(x: Vector2D,y: Vector2D) = VectorOperations.Add ( x , (VectorOperations.Inv y)) static member Sub(x: Vector3D,y: Vector3D) = VectorOperations.Add ( x , (VectorOperations.Inv y))
The code above will work, but requires code duplication, both methods have the same body. If we add an overload for VectorOperations.Add for Vector4D we will have to add the corresponding overload in every chained method to make it work.
With inline functions it’s possible to make them “share” the body, and in this case adding a type Vector4D with the basic operations Add and Inv will automatically make work the function Sub with Vector4D.
. Related or unrelated types
. No “chaining” of overloaded function, the overload will be decided always at each call site.
. Overloading is really ad-hoc, they are just different functions with the same name.
Overloading using inline and hat types
. Static constraints explicit relationship between different overloaded types.
. “Chaining” overloaded functions is possible.
. It’s possible to define generic functions.