views:

195

answers:

4

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?!?

+3  A: 

I don't have F# installed right now, but it seems to me that

MyClass.Add(4, 5) |> printf "%d"

would print 9 whereas

MyClass.Add((4, 5)) |> printf "%d"

would print.. 4 right? Notice the double parantheses, the inner pair marking a tuple and the outer pair marking the function call.

Blindy
Yes, I tested that before asking. But it is still strange if you consider the 3 quotations, isn't it? Were `Add` a static method of a class created by F#, that takes 2 parameters (and would be used as .Add(4, 5) from C#), F# wouldn't compile `.Add(4, 5)`.
Bruno Reis
The middle quote seems somewhat weird, but the last one fits fine with the double parantheses thing. Did you try removing the `Add(int,int)` declaration to see if it resolves to the tuple version then?
Blindy
you are correct
dan
@Blindy: yes, if I remove `Add(int, int)` then F# will happily dispatch `.Add(4, 5)` to C#'s `Add(Tuple<int, int>)`.
Bruno Reis
A: 

I'm not an F# expert, so I may be a bit off base, but I'd guess that the F# concept of a tuple doesn't correlate with the BCL System.Tuple type. Tuples are a core tenet of F# and are built into the language, but C#, VB.NET, and most other .NET languages do not natively support tuples. Since tuples can be useful in these languages, the library is gaining support for them.

I would further my supposition to say that an F# tuple is represented in memory in very much the same way that parameters are passed to methods in C# and friends. That is, they are essentially value arrays of their components. When this value array is pushed onto the stack for a method call, it would have the same effect as pushing each of its constituent components onto the stack, just like when that method is called from C#.

So your second example creates an F# tuple, pushes it onto the stack, then calls the Add overload that takes the types contained within the tuple.

That's my guess, anyway. Having presumably used F# more than I, you might have more insight beyond this. You might also gain additional clues by looking at the generated code Reflector.

P Daddy
Hmmmm, very interesting. I will check the IL generated on many different combinations of F# calls to C# methods and C# calls to F# methods. Thanks!
Bruno Reis
This is wrong. F# tuple _is_ `System.Tuple`. The rest is incorrect conjecture based on the wrong premise.
Pavel Minaev
@Pavel Minaev: I'm in the middle of some test now, and I have experimental observation that *seems* to contradicts you. I will post later, when I finish testing.
Bruno Reis
`printf "%A" ((1, 2).GetType())` is all you need to do to check it. That said, it does of course depends on .NET version - there's no `System.Tuple` prior to .NET 4, so F# running on 3.5 and below will have to use its own type. But, since the question mentions `Tuple` in C# code, it's clear this can only be .NET 4.
Pavel Minaev
+1  A: 

It's just compiler magic.

let add a b = a+b 

add compiles down to add(a,b), making it easy to call from C#. However, F# programs still see it as add a b due to an attribute in the IL.

When calling C# functions in F#, it may help to think of the C# function as having only one parameter -- a tuple, whose elements determine the correct overload. So you can write:

// MyClass.Add(5,3) = 8
let eight = (5,3) |> MyClass.Add
dan
How magic? "Magic"? Or "More Magic"? To me it is "more magic".
Bruno Reis
+6  A: 

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.

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) calls Add(int, int) overload, and Add((1, 2)) calls Add(Tuple<int, int>). There is no ambiguity here.

However, a special case that kicks in for your particular case is this:

If there are no named actual arguments, and there is only one candidate method in M, accepting only one non-optional argument, then the decomposition of arg to tuple form is ignored and there is one named actual arg which is arg itself.

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) and Add(Tuple<int,int>), then a call of the form Add(1,2) wouldn't resolve at all.

Pavel Minaev
Fantastic, Pavel! Thanks!
Bruno Reis