F# lambda与Func的重载决策

8
我正在为记录类型添加一个静态构造方法,代码如下:

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

我可以这样调用它:
let config = ThingConfig.FromSettings mySettingsAccessor

现在是棘手的部分:我想添加第二个重载的构建器,供C#使用(现在忽略重复的实现):
static member FromSettings (getSetting : System.Func<string,string>) : ThingConfig =
    {
        url = getSetting.Invoke "apiUrl";
        token = getSetting.Invoke "apiToken";
    }

这适用于C#,但会破坏我之前的F#调用,出现错误FS0041:“在此程序点之前,无法基于类型信息确定方法'FromSettings'的唯一重载。可能需要类型注释。候选项:静态成员ThingConfig.FromSettings:getSetting:(string -> string) -> ThingConfig,静态成员ThingConfig.FromSettings:getSetting:Func -> ThingConfig”。

为什么F#无法确定要调用哪一个?

那个类型注释会是什么样子?(我能从调用站点注释参数类型吗?)

这种交互是否有更好的模式?(接受来自C#和F#的lambda的重载)


3
虽然这个问题与 https://dev59.com/8eo6XIcBkEYKwwoYQiO7 有关,并且相同的概念适用于两个答案(F#编译器从 Func<_,_>自动转换为.NET委托),但我认为它不足以被视为那个问题的重复,因为两个问题都有各自的用处。 - rmunn
2个回答

11

F#为什么无法确定要调用哪个重载函数?

在F#中,重载决策通常比C#更受限制。为了保证安全性,F#编译器通常会拒绝C#编译器认为是有效的重载。

然而,这种特定情况是真正的歧义。为了利于.NET交互操作,F#编译器对Lambda表达式有一个特殊规定:通常,Lambda表达式将编译为F#函数,但如果期望的类型已知为Func<_,_>,则编译器将把Lambda转换为.NET委托。这使我们可以使用构建在高阶函数(例如IEnumerable<_>)之上的.NET API,而无需手动转换每个Lambda表达式。

因此,在您的情况下,编译器确实感到困惑:您是要将Lambda表达式保留为F#函数并调用F#重载,还是要将其转换为Func<_,_>并调用C#重载?

类型注释会是什么样子?

为帮助编译器,您可以明确指定Lambda表达式的类型为string -> string,如下所示:

let cfg = ThingConfig.FromSettings( (fun s -> foo) : string -> string )
一个稍微更好的方法是将函数定义放在FromSettings调用之外:
let getSetting s = foo
let cfg = ThingConfig.FromSettings( getSetting )

这段代码没问题,因为自动转换为 Func<_,_> 仅适用于内联编写的 Lambda 表达式。编译器不会将任何函数都转换为 .NET 委托。因此,在 FromSettings 调用之外声明 getSetting 可以使其类型明确为 string -> string,从而实现重载决策。


编辑:事实证明上述方法已经失效。当前的 F# 编译器会自动将任何函数转换为 .NET 委托,因此即使将类型指定为 string -> string,仍然存在歧义性。接下来提供其他解决方案。


顺便提一下类型注释,你可以用类似的方式选择另一个重载:

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

或者使用Func构造函数:

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

在这两种情况下,编译器都知道参数的类型是 Func<_,_>,所以可以选择重载。

有更好的模式吗?

重载通常是不好的。它们在某种程度上会掩盖正在发生的事情,导致程序更难调试。我已经数不清有多少个错误,在C#重载决策中选择了IEnumerable而不是IQueryable,从而将整个数据库拉到.NET端。

在这种情况下,我通常会声明两个具有不同名称的方法,然后使用CompiledNameAttribute为它们提供从C#查看时的替代名称。例如:

type ThingConfig = ...

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

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

这样,F# 代码将看到两个方法,FromSettingsFromSettingsCSharp,而 C# 代码将看到相同的两个方法,但命名分别为 FromSettingsFSharpFromSettings。智能感知体验会有点丑陋(但易于理解!),但最终的代码在两种语言中看起来完全一样。

更简单的方法:习惯用法的命名

在 F# 中,以小写字母开头命名函数是惯用法。参见标准库的示例 - Seq.emptyString.concat 等。因此,在您的情况下,我会创建两个方法,一个用于 F# 命名为 fromSettings,另一个用于 C# 命名为 FromSettings

type ThingConfig = ...

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

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

(请注意,第二种方法可以通过第一种方法来实现;您不必复制&粘贴实现)


0

F#中的重载决策存在缺陷。

我已经提交了一些案例,例如这种情况,在这种情况下,它显然与规范相矛盾。

作为解决方法,您可以将C#重载定义为扩展方法:

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

F# 风格的扩展方法在 C# 中不可见,因此这在 F# 中起作用但在 C# 中无用。 - Tarmil
是的,我的答案解决了重载解析问题。关于从C#使用代码方面,有办法使这些扩展对C#可见,但如果没有更多有关在C#中如何使用它们的信息,我无法给出任何建议,一个小代码片段会有所帮助。 - Gus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接