8

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#)

jrr
  • 1,877
  • 19
  • 29
  • 3
    While this question is *related* to https://stackoverflow.com/questions/10110174/best-approach-for-designing-f-libraries-for-use-from-both-f-and-c-sharp, and the same concepts apply to both answers (the F# compiler's auto-conversion from `Func<_,_>` to a .NET delegate), I don't think it's enough of a duplicate of that question that it should be closed. Both questions are useful in their own right. – rmunn Mar 08 '18 at 04:23

2 Answers2

11

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)

Fyodor Soikin
  • 78,590
  • 9
  • 125
  • 172
0

Overload resolution is buggy in F#.

I filed already some cases, like this where it is obviously contradicting the spec.

As a workaround you can define the C# overload as an extension method:

module A =
    type ThingConfig = { url: string; token : string; } with
        static member FromSettings (getSetting : (string -> string)) : ThingConfig =
            printfn "F#ish"
            {
                url = getSetting "apiUrl";
                token = getSetting "apiToken";
            }

module B =
    open A

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

open A
open B

let mySettingsAccessor = fun (x:string) -> x
let mySettingsAccessorAsFunc = System.Func<_,_> (fun (x:string) -> x)
let configA = ThingConfig.FromSettings mySettingsAccessor       // prints F#ish
let configB = ThingConfig.FromSettings mySettingsAccessorAsFunc // prints C#ish
Gus
  • 25,839
  • 2
  • 51
  • 76
  • F#-style extension methods aren't visible from C# though, so this works from F# but is useless from C#. – Tarmil Mar 08 '18 at 13:24
  • Yes, my answer solves the question about the overload resolution problem. Regarding consuming the code from C# there are ways to make these extensions visible to C# but I can't give any advice without more information about how it's planned to be used in C#, a small code fragment would help. – Gus Mar 08 '18 at 15:23