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

Why does declaration order matter for generic members?

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

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())

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

open System

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

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

就像 F# 中的文件顺序很重要一样,行顺序也很重要。通常,文件中后面声明的任何内容都不可用于该文件中前面的表达式。 习惯需要一段时间,但最终是防止您意外编写意大利面条代码的好方法。 有一些例外:

但我认为没有任何机制允许稍后在文件中放置泛型声明,并且可能只是您需要了解的有关 F# 工作原理的信息。

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

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

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

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

相反的顺序,这很好用:

let rec f (arg:'a) = ()
and test() = 
  f 3
  f "boo"

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

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

在第二种情况下,它:

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

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