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#)
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.
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.
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.
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?
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.
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.
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With