Can someone explain these F# type inference oddities?

Viewed 96

I'm experiencing a lot of unexplained behavior with the type inference of F# (or lack of inference rather).

I would expect that all the value functions below would return the correct type T given a Foo of T but in several cases it returns an obj.

type Foo<'T>(v:'T) =
    member __.Value = v // 'T
    static member (~~) (foo:Foo<_>) = foo.Value // Foo<'a> -> 'a

// should know the type but not always!
~~(Foo 1)                                       // int
let value (u:Foo<_>) = u.Value                  // Foo<'a> -> 'a
let value (u:Foo<_>) = ~~u                      // Foo<obj> -> obj
let inline value (u:Foo<_>) = ~~u               // Foo< ^a> -> ^a

let (~~) (foo:Foo<_>) = (Foo<_>.(~~))foo        // Foo<'a> -> 'a
let value (u:Foo<_>) = ~~u                      // Foo<'a> -> 'a

let inline (~~) (x:^a) : ^b = 
    (^a : (static member (~~) : ^a -> ^b) x)    // ^a -> ^b
let value (u:Foo<_>) = ~~u                      // Foo<obj> -> obj

let inline (~~) (x:^a) : 'b = 
    (^a : (static member (~~) : ^a -> 'b) x)    // ^a -> 'b
let value (u:Foo<_>) = ~~u                      // Foo<'a> -> 'a    

What is going on??

1 Answers

I cannot tell why certain functions involving an invocation of your op_TwiddleTwiddle operator definitions cannot be generalized. On the other hand, there are definitively interesting things happening here. Let's start by bringing the code into a form that avoid redefinitions of top-level bindings:

type Foo<'T>(v:'T) =
    member __.Value = v // 'T
    static member (~~) (foo:Foo<_>) = foo.Value     // Foo<'a> -> 'a

module M1 =
    let value (u:Foo<_>) = ~~u                      // Foo<obj> -> obj

module M2 =
    let inline value (u:Foo<_>) = ~~u               // Foo< ^a> -> ^a

module M3 =
    let (~~) (foo:Foo<_>) = (Foo<_>.(~~))foo        // Foo<'a> -> 'a
    let value (u:Foo<_>) = ~~u                      // Foo<'a> -> 'a

module M4 =
    let inline (~~) (x:^a) : ^b = 
        (^a : (static member (~~) : ^a -> ^b) x)    // ^a -> ^b
    let value (u:Foo<_>) = ~~u                      // Foo<obj> -> obj

module M5 =
    let inline (~~) (x:^a) : 'b = 
        (^a : (static member (~~) : ^a -> 'b) x)    // ^a -> 'b
    let value (u:Foo<_>) = ~~u                      // Foo<'a> -> 'a

Looking at the compiled form of the Foo<'T> type, it is clear that the operator takes a type parameter for its return type:

public class Foo<T>
{
    internal T v;

    public T Value
    {
        get
        {
            return this.v;
        }
    }

    public Foo(T v) : this()
    {
        this.v = v;
    }

    public static a op_TwiddleTwiddle<a>(Foo<a> foo)
    {
        return foo.v;
    }
}

This is immaterial as none of the examples except the M2 inline definition do actually call the op_TwiddleTwiddle operator. All the other examples are optimized by direct access to the backing field of the Value property, albeit we would expect it to be transparent, not influencing generalization.

The other interesting cases are

  • M3, where annotation of the operator Foo<_>.(~~) improves the generalization, and
  • M5, in that elimination of the second (unnecessary) statically resolved type variable also makes it work better.
Related