7
votes

I thought that conversions between F# functions and System.Func had to be done manually, but there appears to be a case where the compiler (sometimes) does it for you. And when it goes wrong the error message isn't accurate:

module Foo =
    let dict = new System.Collections.Generic.Dictionary<string, System.Func<obj,obj>>()

    let f (x:obj) = x

    do
        // Question 1: why does this compile without explicit type conversion?
        dict.["foo"] <- fun (x:obj) -> x 
        // Question 2: given that the above line compiles, why does this fail?
        dict.["bar"] <- f 

The last line fails to compile, and the error is:

This expression was expected to have type
    System.Func<obj,obj>    
but here has type
    'a -> obj

Clearly the function f doesn't have a signature of 'a > obj. If the F# 3.1 compiler is happy with the first dictionary assignment, then why not the second?

2
I guess this is a case where the conversion isn't automatic, but the error isn't quite helpful, but is probably still technically correct. - John Palmer
I'd guess only the lambdas get converted automatially. - MisterMetaphor
@MisterMetaphor: correct: stackoverflow.com/questions/3392000/… - Mau
But, wait, why is the type of f and fun (x:obj) -> x not the same? And what's up with the promotion to f : 'a -> obj? - Søren Debois

2 Answers

5
votes

The part of the spec that should explain this is 8.13.7 Type Directed Conversions at Member Invocations. In short, when invoking a member, an automatic conversion from an F# function to a delegate will be applied. Unfortunately, the spec is a bit unclear; from the wording it seems that this conversion might apply to any function expression, but in practice it only appears to apply to anonymous function expressions.

The spec is also a bit out of date; in F# 3.0 type directed conversions also enable a conversion to a System.Linq.Expressions.Expression<SomeDelegateType>.

EDIT

In looking at some past correspondence with the F# team, I think I've tracked down how a conversion could get applied to a non-syntactic function expression. I'll include it here for completeness, but it's a bit of a strange corner case, so for most purposes you should probably consider the rule to be that only syntactic functions will have the type directed conversion applied.

The exception is that overload resolution can result in converting an arbitrary expression of function type; this is partly explained by section 14.4 Method Application Resolution, although it's pretty dense and still not entirely clear. Basically, the argument expressions are only elaborated when there are multiple overloads; when there's just a single candidate method, the argument types are asserted against the unelaborated arguments (note: it's not obvious that this should actually matter in terms of whether the conversion is applicable, but it does matter empirically). Here's an example demonstrating this exception:

type T =
    static member M(i:int) = "first overload"
    static member M(f:System.Func<int,int>) = "second overload"

let f i = i + 1

T.M f |> printfn "%s" 
0
votes

EDIT: This answer explains only the mysterious promotion to 'a -> obj. @kvb points out that replacing obj with int in OPs example still doesn't work, so that promotion is in itself insufficient explanation for the observed behaviour.


To increase flexibility, the F# type elaborator may under certain conditions promote a named function from f : SomeType -> OtherType to f<'a where 'a :> SomeType> : 'a -> OtherType. This is to reduce the need for upcasts. (See spec. 14.4.2.)

Question 2 first:

dict["bar"] <- f                     (* Why does this fail? *)

Because f is a "named function", its type is promoted from f : obj -> obj following sec. 14.4.2 to the seemingly less restrictive f<'a where 'a :> obj> : 'a -> obj. But this type is incompatible with System.Func<obj, obj>.

Question 1:

dict["foo"] <- fun (x:obj) -> x      (* Why doesn't this, then?  *)

This is fine because the anonymous function is not named, and so sec. 14.4.2 does not apply. The type is never promoted from obj -> obj and so fits.


We can observe the interpreter exhibit behaviour following 14.4.2:

> let f = id : obj -> obj
val f : (obj -> obj)                         (* Ok, f has type obj -> obj *)
> f
val it : ('a -> obj) = <fun:it@135-31>       (* f promoted when used. *)

(The interpreter doesn't output constraints to obj.)