如何将C#中的`where T : U`泛型类型参数约束翻译成F#?

16

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 :> ′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! 
}
2个回答

9

我认为在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: 无效约束:用于约束的类型是封闭的,这意味着该约束最多只能满足一个解决方案

这似乎与在此邮件列表中讨论的情况相同。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 }

1
好的,我现在相当确信没有简洁的解决方案可以实现我想做的事情。为finallyAction指定 obj 会带来一个令人讨厌的副作用,即将自定义绑定(let!)到类型为obj,这意味着我不能再对它们进行有意义的处理了。需要进一步思考如何以不同的方式实现该构建器。但我希望他们能在未来版本的 F# 语言中修复这个问题... - stakx - no longer contributing
1
@stakx:即使您使用obj,使用let!绑定的值的类型也应该是实际(更具体)的类型。我编辑了答案,包括演示这一点的示例(我最终也测试了它:-))。 - Tomas Petricek
你真是个巫师! :) 我猜那些类型注释阻碍了你。谢谢! - stakx - no longer contributing
@stakx:我很惊讶它甚至不需要任何类型注释就能运行。我猜F#就是一门神奇的语言... :-) - Tomas Petricek

3
它将会是类似于这样的东西。
...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() = ()

这会丢失类型安全(如果事物不是最终可操作的,则在运行时将抛出强制转换异常)。

或者您可以创建特定于类型的构建器:

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 - stakx - no longer contributing
谢谢您的回复。我可以通过将某些类型变量固定为具体类型来解决我的问题。我有点失望地发现,F#拥有所有这些美妙的类型推断功能,却不能像C#那样轻松地做一些事情...这让我感到意外。 - stakx - no longer contributing
“但我想我已经没有其他聪明的想法了。” 尽管如此,我非常感谢您的努力。;-) 我整天都在尝试解决这个问题,但是没有找到令人满意的解决方案,所以看到其他人也在为此苦苦挣扎,至少让我放心我没有错过什么非常基本的东西。 - stakx - no longer contributing

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