2

(Note: Question updated with full reproducible example)

After migrating an F# project from VS 2012 to VS 2015 I receive an error on certain usages of interfaces. In particular it happens where a type implements two generic interfaces. I know this is not allowed in F# directly, but this type comes from C#.

To reproduce the problem:

1. type definition in C#:

Paste this in some class library.

public interface Base { }

public interface IPrime<T> : Base
{
    T Value { get; }
}


public interface IFloat : IPrime<double>
{
}

public interface IInt : IFloat, IPrime<int>
{
    int Salary { get; }
}

public abstract class Prime<T> : IPrime<T>
{
    public T Value { get; protected internal set; }

    public static implicit operator T(Prime<T> value)
    {
        return value.Value;
    }
}


public class FFloat : Prime<double>, IFloat
{
    public FFloat(double value)
    {
        this.Value = value;
    }
    public double Salary { get; set; }
}

public class FInt : Prime<int>, IInt
{
    public FInt(int value)
    {
        this.Value = value;
    }
    public int Salary { get; set; }
    int IPrime<int>.Value { get { return this.Value; } }
    double IPrime<double>.Value { get { return this.Value; } }
}

2. Usage of types in F#:

Usage in Visual Studio 2012, working:

open SomeClassLib

[<EntryPoint>]
let main argv = 
    let i = new FInt(10)
    let f = new FFloat(12.0)
    let g = fun (itm: SomeClassLib.Base) -> 
        match itm with
        | :? IInt as i -> i.Value
        | :? IFloat as i -> i.Value |> int
        | _ -> failwith "error"

If you open the same solution in Visual Studio 2015 you get the error

error FS0001: Type mismatch. Expecting a float -> float but given a float -> int.The type 'float' does not match the type 'int'

This is of course easily corrected by a typecast, but then, surprise surprise, it won't load in Visual Studio 2012 anymore (well, there are ways to get it working in both, and in this example it is trivial).

3. Findings

If you hover over the i.Value you get:

Visual Studio 2012

| :? IInt as i -> i.Value          // Value is int
| :? IFloat as i -> i.Value |> int // Value is float

Visual Studio 2015

| :? IInt as i -> i.Value          // Value is float
| :? IFloat as i -> i.Value |> int // Value is float

I'm wondering where this difference comes from.

Questions / remarks

I find it strange that the compiler seems to "choose" that one assignment works, and another does not, this invites for inadvertent and sometimes hard-to-get mistakes. And frankly, I'm a little worried that in places were type inference can choose between int and float, it will now favor float, where it was int in the past.

The difference between the 2012 and 2015 seems to be that the former takes the first in encounters when going up the hierarchy, and the latter seems to take the last, but I haven't been able to unambiguously confirm that.

Is this a bug or an improvement of an existing feature? I'm afraid I'll have some redesigning to do to remove the ambiguity, unless someone knows of a simple way to deal with this (it happens in only about 50 or so places, doable to fix by hand, but not so nice)?

Preliminary conclusion

It is clear to me that the original type can be considered ambiguous and possibly poor design, but .NET languages support it and so does MSIL.

I know that F# does not support mixing generic types on the same method or property, which is a language choice I can live with, but its type inference makes a decision that cannot be predicted, which I think is a bug.

Either way, I believe this should be an error, similar to the one you get when you address overloaded members in F#, in which case the error is very clear and lists the options.

ildjarn
  • 62,044
  • 9
  • 127
  • 211
Abel
  • 56,041
  • 24
  • 146
  • 247
  • I think you should remove the ambiguity, regardless of whether there is a "nice" solution. Also, how did this work in C#? Since C# has implicit type conversion between `int` and `double`, aren't you worried about this happening in C# in the first place? – Fyodor Soikin Nov 04 '15 at 23:20
  • Assignments in F# are '<-' and not '='. Is that a typo? – hocho Nov 04 '15 at 23:30
  • Because both interfaces are been implemented by the C# type, they can't have 2 properties called `add`. One (or both) will have to be an explicit interface implementation (e.g. `float ITest.Add { get; set; }`). Can you post the C# implementation of the `IWithBoth`? – DaveShaw Nov 04 '15 at 23:33
  • @Daveshaw, let me check (old library, we usually use the compiled version). And yes, I think you are right. \@Fyodor, I agree wholeheartedly, but sometimes we are limited by budgets and can't do a redesign of frozen libraries that are also used in other projects or customers... – Abel Nov 04 '15 at 23:35
  • Interesting bit is that the code gives a rather explicit error on `IWithBoth` definition when pasted directly into interactive : *error FS0443: This type implements the same interface at different generic instantiations `ITest` and `ITest`. This is not permitted in this version of F#.* you should take a look [there](http://stackoverflow.com/questions/1464109/implementing-the-same-interface-at-different-generic-instantiations) not sure if it qualify as a duplicate though – Sehnsucht Nov 05 '15 at 00:12
  • @DaveShaw, I have updated my question, there's now a fully reproducible example (though the explicit interfaces do not appear in the original code, I don't know why it compiles there just fine). – Abel Nov 05 '15 at 00:44
  • @Sehnsucht: yes, indeed, that is the error you will get, but as the (original before edit, but also now) question said was that the provided F# code was just illustrating the interfaces as they appear in C#. I have updated now. – Abel Nov 05 '15 at 00:46

1 Answers1

1

I swapped the order of the interfaces in the C# code to this and the following code compiled as in VS 2013

public interface IInt : IPrime<int>, IFloat
{
   int Salary { get; }
}
let g = fun (itm: Base) -> 
    match itm with
    | :? IInt as i -> i.Value
    | :? IFloat as i -> i.Value |> int
    | _ -> failwith "error"

I suspect that one of the 'Value' members is hidden (shadowed by FSharp's perspective) by the other, based on the order of the interfaces.

I coded your pattern matching like this instead, and validated that the interface order did not matter, in this case.

let g = fun (itm: Base) -> 
    match itm with
    | :? IPrime<int> as i -> i.Value
    | :? IPrime<float> as i -> i.Value |> int
    | _ -> failwith "error"

To me, this seems like a change in the implementation details. I am not sure if I would call it a bug. The F# specification would be the final word if this is a bug.

hocho
  • 1,753
  • 1
  • 11
  • 14
  • Thanks. I am _very_ surprised to see that the order of interfaces matters, that's a big (dis)advantage, very shady. It helps as a quick solution. And just tried it, instead of 100s of cast errors, I ended up with zero, great! (though I still think this is a bug of F#) (ps, apologies for my prev. comment, I spoke too hastily). – Abel Nov 05 '15 at 01:46
  • Hmm, important detail to add: after swapping the order of the comma-separated list of interfaces, it compiles fine in VS2015, **but not anymore in VS2012**. Luckily we do not intend to maintain both versions. This may also point out that the cause lies in the C# 4 vs C# 5 compiler, not sure. – Abel Nov 05 '15 at 01:53