Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

F# Method overload resolution not as smart as C#?

Tags:

f#

Say, I have

member this.Test (x: 'a) = printfn "generic"
                           1
member this.Test (x: Object) = printfn "non generic"
                               2

If I call it in C#

var a = test.Test(3);         // calls generic version
var b = test.Test((object)3); // calls non generic version
var c = test.Test<object>(3); // calls generic version

However, in F#

let d = test.Test(3);  // calls non generic version
let e = test.Test<int>(3); // calls generic version

So I have to add type annotation so as to get the correct overloaded method. Is this true? If so, then why F# doesn't automatically resolve correctly given that the argument type is already inferred? (what is the order of F#'s overload resolution anyway? always favor Object than its inherited classes?)

It is a bit dangerous if a method has both overloads, one of them takes argument as Object type and the other one is generic and both return the same type. (like in this example, or Assert.AreEqual in unit testing), as then it is very much likely we get the wrong overloading without even notice (won't be any compiler error). Wouldn't it be a problem?

Update:

Could someone explain

  • Why F# resolves Assert.AreEqual(3, 5) as Assert.AreEqual(Object a, Object b) but not Assert.AreEqual<T>(T a, T b)

  • But F# resolves Array.BinarySearch([|2;3|], 2) as BinarySearch<T>(T[]array, T value) but not BinarySearch(Array array, Object value)

like image 866
colinfang Avatar asked Jan 31 '13 00:01

colinfang


1 Answers

F# Method overload resolution not as smart as C#?

I don't think it's true. Method overloading makes type inference much more difficult. F# has reasonable trade-offs to make method overloading usable and type inference as powerful as it should be.

When you pass a value to a function/method, F# compiler automatically upcasts it to an appropriate type. This is handy in many situations but also confusing sometimes.

In your example, 3 is upcasted to obj type. Both methods are applicable but the simpler (non-generic) method is chosen.

Section 14.4 Method Application Resolution in the spec specifies overloading rules quite clearly:

1) Prefer candidates whose use does not constrain the use of a user-introduced generic type annotation to be equal to another type.

2) Prefer candidates that do not use ParamArray conversion. If two candidates both use ParamArray conversion with types pty1 and pty2, and pty1 feasibly subsumes pty2, prefer the second; that is, use the candidate that has the more precise type.

3) Prefer candidates that do not have ImplicitlyReturnedFormalArgs.

4) Prefer candidates that do not have ImplicitlySuppliedFormalArgs.

5) If two candidates have unnamed actual argument types ty11...ty1n and ty21...ty2n, and each ty1i either

a. feasibly subsumes ty2i, or

b. ty2i is a System.Func type and ty1i is some other delegate type, then prefer the second candidate. That is, prefer any candidate that has the more specific actual argument types, and consider any System.Func type to be more specific than any other delegate type.

6) Prefer candidates that are not extension members over candidates that are.

7) To choose between two extension members, prefer the one that results from the most recent use of open.

8) Prefer candidates that are not generic over candidates that are generic—that is, prefer candidates that have empty ActualArgTypes.

I think it's users' responsibility to create unambiguous overloaded methods. You can always look at inferred types to see whether you're doing them correctly. For example, a modified version of yours without ambiguity:

type T() =
    member this.Test (x: 'a) = printfn "generic"; 1
    member this.Test (x: System.ValueType) = printfn "non-generic"; 2

let t = T()
let d = t.Test(3)  // calls non-generic version
let e = t.Test(test) // call generic version

UPDATE:

It comes down a core concept, covariance. F# doesn't support covariance on arrays, lists, functions, etc. It's generally a good thing to ensure type safety (see this example).

So it's easy to explain why Array.BinarySearch([|2;3|], 2) is resolved to BinarySearch<T>(T[] array, T value). Here is another example on function arguments where

T.Test((fun () -> 2), 2)

is resolved to

T.Test(f: unit -> 'a, v: 'a)

but not to

T.Test(f: unit -> obj, v: obj)
like image 154
pad Avatar answered Nov 14 '22 16:11

pad