【问题标题】:overload resolution of F# lambda vs FuncF# lambda vs Func 的重载解析
【发布时间】:2018-03-08 03:30:39
【问题描述】:

我正在向这样的记录类型添加静态构建器方法:

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)

【问题讨论】:

  • 虽然这个问题与stackoverflow.com/questions/10110174/…相关,但相同的概念适用于两个答案(F# 编译器从Func&lt;_,_&gt; 自动转换为 .NET 委托) ,我认为应该关闭该问题的副本还不够。这两个问题本身都很有用。

标签: f#


【解决方案1】:

为什么 F# 不能确定调用哪一个?

F# 中的重载分辨率通常比 C# 更受限制。为了安全起见,F# 编译器通常会拒绝 C# 编译器认为有效的重载。

但是,这个具体案例确实是模棱两可的。为了 .NET 互操作,F# 编译器对 lambda 表达式有一个特殊规定:通常,一个 lambda 表达式将被编译为一个 F# 函数,但如果已知预期类型是 Func&lt;_,_&gt;,编译器将转换 lambda给 .NET 委托。这允许我们使用基于高阶函数(例如 IEnumerable&lt;_&gt;(又名 LINQ))构建的 .NET API,而无需手动转换每个 lambda。

所以在你的情况下,编译器真的很困惑:你的意思是将 lambda 表达式保留为 F# 函数并调用你的 F# 重载,还是要将其转换为 Func&lt;_,_&gt; 并调用 C# 重载?

类型注释会是什么样子?

为了帮助编译器,您可以将 lambda 表达式的类型显式声明为 string -&gt; string,如下所示:

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

更好的方法是在 FromSettings 调用之外定义函数:

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

这很好用,因为自动转换为Func&lt;_,_&gt; 仅适用于内联编写的 lambda 表达式。编译器不会将任何函数转换为 .NET 委托。因此,在 FromSettings 调用之外声明 getSetting 使其类型明确为 string -&gt; string,并且重载解析起作用。


编辑:事实证明,上述内容不再有效。当前的 F# 编译器将自动将任何函数转换为 .NET 委托,因此即使将类型指定为 string -&gt; string 也不会消除歧义。继续阅读以了解其他选项。


说到类型注解——你可以用类似的方式选择另一个重载:

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

或者使用Func构造函数:

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

在这两种情况下,编译器都知道参数的类型是Func&lt;_,_&gt;,因此可以选择重载。

有更好的模式吗?

重载通常很糟糕。它们在某种程度上掩盖了正在发生的事情,使程序更难调试。我已经忘记了 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# 代码将看到两个方法,FromSettings 和 FromSettingsCSharp,而 C# 代码将看到相同的两个方法,但分别命名为 FromSettingsFSharp 和 FromSettings。智能感知体验会有点难看(但很容易理解!),但完成的代码在两种语言中看起来完全一样。

更简单的选择:惯用命名

在 F# 中,将函数命名为第一个字符小写是惯用的。有关示例,请参阅标准库 - Seq.empty、String.concat 等。所以在您的情况下我会实际上做什么,我会创建两种方法,一种用于名为 fromSettings 的 F#,另一种对于名为 FromSettings 的 C#:

type ThingConfig = ...

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

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

(另请注意,第二种方法可以根据第一种方法实现;您不必复制和粘贴实现)

【讨论】:

    【解决方案2】:

    重载分辨率在 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# 中无用。
    • 是的,我的回答解决了关于重载解决问题的问题。关于使用 C# 中的代码,有一些方法可以使这些扩展对 C# 可见,但如果没有更多关于如何在 C# 中使用它的信息,我无法给出任何建议,一个小的代码片段会有所帮助。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-05-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-09
    相关资源
    最近更新 更多