首页 文章

F#lambda vs 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#,但是使用错误FS0041中断了我之前的F#调用:无法根据此程序点之前的类型信息确定方法'FromSettings'的唯一重载 . 可能需要类型注释 . 候选人:静态成员ThingConfig.FromSettings:getSetting:(string - > string) - > ThingConfig,静态成员ThingConfig.FromSettings:getSetting:Func - > ThingConfig

为什么F#不知道要拨打哪一个?

该类型注释会是什么样的? (我可以从呼叫站点注释参数类型吗?)

这种互操作是否有更好的模式? (重载从C#和F#接受lambdas)

2 回答

  • 11

    为什么F#不知道要拨打哪一个?

    F#中的过载分辨率通常比C#更有限 . 出于安全考虑,F#编译器通常会拒绝C#编译器认为有效的重载 .

    但是,这个具体案例是一种真正的模棱两可 . 为了.NET interop的利益,F#编译器对lambda表达式有一个特殊的规定:有规律地,lambda表达式将被编译为F#函数,但是如果预期的类型是 Func<_,_> ,编译器会将lambda转换为a .NET委托 . 这允许我们使用基于高阶函数构建的.NET API,例如 IEnumerable<_> (又名LINQ),而无需手动转换每个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 . intellisense体验将有点难看(但很容易理解!),但完成的代码在两种语言中看起来都完全相同 .

    更简单的替代方案:惯用命名

    在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
    

相关问题