为什么声明顺序对通用成员很重要?

Asi*_*sik 2 f#

今天我注意到以下内容无法编译:

open System

type MyType() =        
    member this.Something() =
        this.F(3)
        this.F("boo") 
        //     ^^^^^
        // This expression was expected to have type 'int' but here has type 'string'
        
    member private this.F<'T> (t:'T) =
        //                ^^
        // This type parameter has been used in a way that constrains it to always be 'int'
        // This code is less generic than required by its annotations because the explicit type variable 'T' could not be generalized. It was constrained to be 'int'.
        Console.WriteLine (t.GetType())
Run Code Online (Sandbox Code Playgroud)

但是只要改变声明顺序,就没有问题。

open System

type MyType() =        
    member private this.F<'T> (t:'T) =
        Console.WriteLine (t.GetType())
        
    member this.Something() =
        this.F(3)
        this.F("boo")
Run Code Online (Sandbox Code Playgroud)

这花了我很长时间才弄清楚,因为我没想到声明顺序对班级成员很重要。这是预期的行为吗?

Tom*_*cek 6

这是 F# 类型推断工作方式的一个微妙的副作用。我认为没有比重新排序您的定义更好的解决方法。

为了提供更多背景信息,类的成员被自动视为相互递归(意味着它们可以相互调用)。与模块中的多个类型或多个函数不同,您不需要使用rec关键字显式声明它。

然而,问题不仅限于类。您可以使用简单的函数获得完全相同的行为。最简单的例子是:

let rec test() = 
  f 3       // Warning: causes code to be less generic
  f "boo"   // Error: string does not match int
and f (arg:'a) = ()
Run Code Online (Sandbox Code Playgroud)

以相反的顺序,这很好用:

let rec f (arg:'a) = ()
and test() = 
  f 3
  f "boo"
Run Code Online (Sandbox Code Playgroud)

问题是类型检查器从上到下分析代码。在第一种情况下,它:

  • 看到f 3并推断出f它的类型int -> unit
  • 查看f "boo"并报告类型错误
  • 看到f (arg:'a)并意识到它过早地使用了比需要的更具体的类型(并报告了各种警告)。

在第二种情况下,它:

  • 看到f (arg:'a)并推断出f它的类型'a -> unit
  • 查看f 3f "boo"使用适当的类型实例化

类型检查器不能更聪明的主要原因是类型检查器分析了整个函数体(或整个递归块)后才进行“泛化”(即确定函数是泛型的)。如果它在体内遇到更具体的用途,它永远不会进入这个泛化步骤。