I’ve read many times that
Assemblies generated from F# or any other .NET language are (almost) indistinguishable.
I was then experimenting with F# and C# interop on .NET 4 (beta 2). I created a new solution, and a C# project, with the following class:
public class MyClass {
public static int Add(int a, int b) { return a + b; }
}
Then, on a F# project, after referencing the C# project, I tried:
MyClsas.Add(4, 5) |> printfn "%d" // prints 9 (no kidding!)
So far so good. Then another sentence I’ve read many times (perhaps on different books) came to my mind:
When passing arguments to functions from other .NET libraries, you use a syntax like “.MethodName(parm1, parm2)”, that is, the parameters are passed as a Tuple.
Add that to something that I’ve once read here on SO (but wasn’t able find it to link to), on a question where the OP was trying to create a using like [ 4, 5, 6 ] (when he meant [4; 5; 6]):
“Comma is the ‘tuple creating operator’, for everything else use semi-colon.”
Then I modified my class to the following:
public class MyClass {
public static int Add(int a, int b) { return a + b; }
public static int Add(Tuple<int, int> a) { return a.Item1; }
}
Now I tried to use it on F#:
MyClass.Add(4, 5) |> printf "%d" // prints ... (keep reading!)
So, adding up the three quotations above, one can conclude that:
- F# will create a Tuple when it sees
(4, 5) - Then it will call the overload
Add(Tuple<int, int>) - So it will print 4
To my surprise, it printed 9. Isn’t it interesting?
What is really happening here? The above quotations and this practical observations seems to be in contradiction. Can you justify F#’s “reasoning”, and maybe pointing to some MSDN docs if possible?
Thanks!
EDIT
(to add more information (from Blindy’s answer))
If you do:
MyClass.Add((4, 5)) |> printfn "%d" // prints 9
F# calls the Add(Tuple<int, int>) overload.
However, if you create another F# project (so a different assembly) with this:
namespace MyFSharpNamespace
type MyFShapClass = class
static member Add x y = x + y
end
You can use it on C# like this
public static void Main(string[] args) {
MyFSharpNamespace.MyFSharpClass.Add(4, 5);
}
So far so good. Now, when you try to use it from F# (from another project, another assembly), you have to do:
MyFSharpNamespace.MyFSharpClass.Add 4 5 |> printfn "%d"
If you pass the arguments as (4, 5) F# will not compile because Add is int -> int -> int, and not (int * int) -> int.
What is happening?!?
It’s more hideous than that. See the description of method overload resolution strait from the language spec.
What it says, basically, is that argument in a method invocation isn’t really a tuple. It’s a syntactic tuple, meaning a comma-separated list of something, but the parentheses are part of the method call syntax, and so are the commas. It’s why, for example,
o.M(a=1, b=2)isn’t a method call with a tuple of two booleans, but rather two named arguments.So, normally, every comma-separated component just maps to a distinct argument. Hence why
Add(1, 2)callsAdd(int, int)overload, andAdd((1, 2))callsAdd(Tuple<int, int>). There is no ambiguity here.However, a special case that kicks in for your particular case is this:
So when you removed all overloads except for the tuple one, suddenly the entire thing inside the parentheses is effectively treated as a tuple constructor in a call. But if you’d e.g. have two overloads,
Add(int)andAdd(Tuple<int,int>), then a call of the formAdd(1,2)wouldn’t resolve at all.