Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

overload resolution of F# lambda vs Func

Tags:

f#

I'm adding a static builder method to a record type like this:

type ThingConfig = { url: string; token : string; } with
    static member FromSettings (getSetting : (string -> string)) : ThingConfig =
        {
            url = getSetting "apiUrl";
            token = getSetting "apiToken";
        }

I can call it like this:

let config = ThingConfig.FromSettings mySettingsAccessor

Now the tricky part: I'd like to add a second overloaded builder for use from C# (ignore the duplicated implementation for now):

static member FromSettings (getSetting : System.Func<string,string>) : ThingConfig =
    {
        url = getSetting.Invoke "apiUrl";
        token = getSetting.Invoke "apiToken";
    }

This works for C#, but breaks my earlier F# call with error FS0041: A unique overload for method 'FromSettings' could not be determined based on type information prior to this program point. A type annotation may be needed. Candidates: static member ThingConfig.FromSettings : getSetting:(string -> string) -> ThingConfig, static member ThingConfig.FromSettings : getSetting:Func -> ThingConfig

Why can't F# figure out which one to call?

What would that type annotation look like? (Can I annotate the parameter type from the call site?)

Is there a better pattern for this kind of interop? (overloads accepting lambdas from both C# and F#)

like image 473
jrr Avatar asked Mar 08 '18 03:03

jrr


People also ask

What is the overload resolution?

The process of selecting the most appropriate overloaded function or operator is called overload resolution. Suppose that f is an overloaded function name. When you call the overloaded function f() , the compiler creates a set of candidate functions.

What does overload resolution failed mean?

It generates this error message when one overload is more specific for one argument's data type while another overload is more specific for another argument's data type.

What is overload function in C?

Function overloading is a feature of object-oriented programming where two or more functions can have the same name but different parameters. When a function name is overloaded with different jobs it is called Function Overloading.


1 Answers

Why can't F# figure out which one to call?

Overload resolution in F# is generally more limited than C#. The F# compiler will often, in the interest of safety, reject overloads that C# compiler sees as valid.

However, this specific case is a genuine ambiguity. In the interest of .NET interop, F# compiler has a special provision for lambda expressions: regularly, a lambda expression will be compiled to an F# function, but if the expected type is known to be Func<_,_>, the compiler will convert the lambda to a .NET delegate. This allows us to use .NET APIs built on higher-order functions, such as IEnumerable<_> (aka LINQ), without manually converting every single lambda.

So in your case, the compiler is genuinely confused: did you mean to keep the lambda expression as an F# function and call your F# overload, or did you mean to convert it to Func<_,_> and call the C# overload?

What would the type annotation look like?

To help the compiler out, you can explicitly state the type of the lambda expression to be string -> string, like so:

let cfg = ThingConfig.FromSettings( (fun s -> foo) : string -> string )

A slightly nicer approach would be to define the function outside of the FromSettings call:

let getSetting s = foo
let cfg = ThingConfig.FromSettings( getSetting )

This works fine, because automatic conversion to Func<_,_> only applies to lambda expressions written inline. The compiler will not convert just any function to a .NET delegate. Therefore, declaring getSetting outside of the FromSettings call makes its type unambiguously string -> string, and the overload resolution works.


EDIT: it turns out that the above no longer actually works. The current F# compiler will convert any function to a .NET delegate automatically, so even specifying the type as string -> string doesn't remove the ambiguity. Read on for other options.


Speaking of type annotations - you can choose the other overload in a similar way:

let cfg = ThingConfig.FromSettings( (fun s -> foo) : Func<_,_> )

Or using the Func constructor:

let cfg = ThingConfig.FromSettings( Func<_,_>(fun s -> foo) )

In both cases, the compiler knows that the type of the parameter is Func<_,_>, and so can choose the overload.

Is there a better pattern?

Overloads are generally bad. They, to some extent, obscure what is happening, making for programs that are harder to debug. I've lost count of bugs where C# overload resolution was picking IEnumerable instead of IQueryable, thus pulling the whole database to the .NET side.

What I usually do in these cases, I declare two methods with different names, then use CompiledNameAttribute to give them alternative names when viewed from C#. For example:

type ThingConfig = ...

    [<CompiledName "FromSettingsFSharp">]
    static member FromSettings (getSetting : (string -> string)) = ...

    [<CompiledName "FromSettings">]
    static member FromSettingsCSharp (getSetting : Func<string, string>) = ...

This way, the F# code will see two methods, FromSettings and FromSettingsCSharp, while C# code will see the same two methods, but named FromSettingsFSharp and FromSettings respectively. The intellisense experience will be a bit ugly (yet easily understandable!), but the finished code will look exactly the same in both languages.

Easier alternative: idiomatic naming

In F#, it is idiomatic to name functions with first character in the lower case. See the standard library for examples - Seq.empty, String.concat, etc. So what I would actually do in your situation, I would create two methods, one for F# named fromSettings, the other for C# named FromSettings:

type ThingConfig = ...

    static member fromSettings (getSetting : string -> string) = 
        ...

    static member FromSettings (getSetting : Func<string,string>) = 
        ThingConfig.fromSettings getSetting.Invoke

(note also that the second method can be implemented in terms of the first one; you don't have to copy&paste the implementation)

like image 104
Fyodor Soikin Avatar answered Nov 10 '22 00:11

Fyodor Soikin