【问题标题】:How do I translate a `where T : U` generic type parameter constraint from C# to F#?如何将 `where T : U` 泛型类型参数约束从 C# 转换为 F#?
【发布时间】:2010-11-13 15:37:08
【问题描述】:

F# 的类型推断规则给我带来了一些麻烦。我正在编写一个简单的计算构建器,但无法正确设置我的泛型类型变量约束。


我想要的代码在 C# 中如下所示:

class FinallyBuilder<TZ>
{
    readonly Action<TZ> finallyAction;

    public FinallyBuilder(Action<TZ> finallyAction)
    {
        this.finallyAction = finallyAction;
    }

    public TB Bind<TA, TB>(TA x, Func<TA, TB> cont)  where TA : TZ
    {                                      //        ^^^^^^^^^^^^^
        try                                // this is what gives me a headache
        {                                  //      in the F# version
            return cont(x);
        }
        finally
        {
            finallyAction(x);
        }
    }
}

到目前为止,我为 F# 版本 提出的最佳(但非编译代码)是:

type FinallyBuilder<′z> (finallyAction : ′z -> unit) =

    member this.Bind (x : ′a) (cont : ′a -> ′b) =
        try     cont x
        finally finallyAction (x :> ′z) // cast illegal due to missing constraint

// Note: ' changed to ′ to avoid bad syntax highlighting here on SO.

不幸的是,我不知道如何翻译Bind 方法上的where TA : TZ 类型约束。我认为它应该类似于 ′a when ′a :&gt; ′z,但 F# 编译器在任何地方都不喜欢这样,而且我总是会得到一些泛型类型变量约束到另一个。

谁能告诉我正确的 F# 代码?


背景:我的目标是能够编写这样的 F# 自定义工作流:

let cleanup = new FinallyBuilder (fun x -> ...)

cleanup {
    let! x = ...   // x and y will be passed to the above lambda function at
    let! y = ...   // the end of this block; x and y can have different types! 
}

【问题讨论】:

    标签: c# generics f# type-constraints computation-expression


    【解决方案1】:

    我认为不可能在 F# 中编写这样的约束(尽管我不确定为什么)。无论如何,从语法上讲,你会想写这样的东西(正如 Brian 建议的那样):

    type FinallyBuilder<'T> (finallyAction : 'T -> unit) = 
      member this.Bind<'A, 'B when 'A :> 'T>(x : 'A) (cont : 'A -> 'B) =  //' 
        try cont x 
        finally finallyAction (x :> 'T) 
    

    不幸的是,这会产生以下错误:

    error FS0698: Invalid constraint: 用于约束的类型是密封的,这意味着约束只能由最多一种解决方案满足

    这似乎与this mailing list 中讨论的情况相同。 Don Syme 在哪里说:

    这是为使 F# 类型推断易于处理而施加的限制。特别是,子类型约束右侧的类型必须是名义上的。注意 'A :> 'B 形式的约束总是急切地求解为 'A = 'B,如 F# 规范的第 14.5.2 节(求解子类型约束)中所述。

    您始终可以通过在传递给构建器的函数中使用obj 来解决此问题。
    编辑:即使您使用obj,使用let! 绑定的值也会有更具体的类型(调用finallyAction时,F#会自动将某个类型参数的值转换为obj):

    type FinallyBuilder(finallyAction : obj -> unit) =  
      member x.Bind(v, f) =  
        try f v 
        finally finallyAction v 
      member x.Return(v) = v
    
    let cleanup = FinallyBuilder(printfn "%A")
    
    let res = 
      cleanup { let! a = new System.Random()
                let! b = "hello"
                return 3 }
    

    【讨论】:

    • 好的,我现在相当确信,我想做的事情没有完美的解决方案。为 finallyAction 指定 obj 会产生令人讨厌的副作用,即会将自定义工作流程中的所有自定义绑定 (let!) 值减少为键入 obj,这意味着我无法再对它们进行明智的处理.需要更多地考虑如何以不同的方式实现该构建器。但我希望他们能在 F# 语言的未来版本中解决这个问题......
    • @stakx:即使您使用obj,使用let! 绑定的值的类型也应该是实际(更具体的)类型。我编辑了答案以包含一个演示这一点的示例(我最后也对其进行了测试:-))。
    • 你是个巫师! :) 我猜所有这些类型注释都妨碍了。谢谢!
    • @stakx:我很惊讶它在没有任何类型注释的情况下也能工作。我猜 F# 只是一种神奇的语言... :-)
    【解决方案2】:

    会是这样的

    ...Bind<'A when 'A :> 'Z>...
    

    但让我编写代码以确保它完全正确...

    啊,好像是这样的:

    type FinallyBuilder<'z> (finallyAction : 'z -> unit) = 
        member this.Bind<'a, 'b when 'a :> 'z> (x : 'a, cont : 'a -> 'b) : 'b = 
            try     cont x 
            finally finallyAction x //(x :> 'z)// illegal 
    

    除了

    http://cs.hubfs.net/forums/thread/10527.aspx

    指出 F# 不执行“T1 :> T2”形式的约束,其中两者都是类型变量(它假定 T1 = T2)。但是,这对于您的情况可能没问题,您究竟打算将什么用作Z 的具体实例?可能有一个简单的解决方法或一些不太通用的代码可以满足这种情况。例如,我想知道这是否有效:

    type FinallyBuilder<'z> (finallyAction : 'z -> unit) = 
        member this.Bind<'b> (x : 'z, cont : 'z -> 'b) : 'b = //'
            try     cont x 
            finally finallyAction x 
    

    似乎:

    type FinallyBuilder<'z> (finallyAction : 'z -> unit) = 
        member this.Bind<'b> (x : 'z, cont : 'z -> 'b) : 'b = // '
            try     cont x 
            finally finallyAction x 
        member this.Zero() = ()
    
    [<AbstractClass>]
    type Animal() =
        abstract Speak : unit -> unit
    
    let cleanup = FinallyBuilder (fun (a:Animal) -> a.Speak())
    
    type Dog() =
        inherit Animal()
        override this.Speak() = printfn "woof"
    
    type Cat() =
        inherit Animal()
        override this.Speak() = printfn "meow"
    
    cleanup {
        let! d = new Dog()
        let! c = new Cat()
        printfn "done"
    }
    // prints done meow woof
    

    哦,我明白了,但是 dc 现在有类型 Animal。嗯,让我看看我还有没有什么聪明之处……

    好吧,显然你可以做到

    type FinallyBuilder<'z> (finallyAction : 'z -> unit) = 
        member this.Bind<'a,'b> (x : 'a, cont : 'a -> 'b) : 'b = // '
            try     cont x 
            finally finallyAction (x |> box |> unbox)
        member this.Zero() = ()
    

    这会抛弃类型安全(如果事物不是 finallyActionable,将在运行时抛出强制转换异常)。

    或者您可以制作特定类型的构建器:

    type FinallyBuilderAnimal (finallyAction : Animal -> unit) = 
        member this.Bind<'a,'b when 'a:>Animal>(x : 'a, cont : 'a -> 'b) : 'b = //'
            try     cont x 
            finally finallyAction x
        member this.Zero() = ()
    
    let cleanup = FinallyBuilderAnimal (fun a -> a.Speak())
    

    但我认为我没有其他聪明的想法。

    【讨论】:

    • "这种结构导致代码比类型注释所指示的更通用。类型变量'a 已被限制为类型'z。" :- (
    • 感谢您的回复。我也许可以通过将某些类型变量修复为具体类型来解决我的问题。我有点失望地发现具有所有这些美妙类型推断的 F# 无法完成 C# 可以轻松完成的事情……这是出乎意料的。
    • “但我认为我没有其他聪明的想法。” 尽管如此,我还是很感谢你的努力。你一定对此很感兴趣! ;-) 我整天都在玩这个,但还没有找到一个令人满意的解决方案,所以看到其他人为此苦苦挣扎,至少让我放心,我没有错过一些非常基本的东西。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-11-19
    • 2017-01-21
    • 1970-01-01
    相关资源
    最近更新 更多