什么是泛型的F#类型推断方法?

时间:2019-05-25 11:48:58

标签: f# type-inference typechecking static-typing hindley-milner

我想了解有关类型推断的规则,因为我希望将其纳入自己的语言,因此我一直在研究F#的类型推断,以下内容使我感到奇怪。 / p>

这将进行编译,并且id'a -> 'a,这(如果我没记错的话)意味着每次调用都使用“新鲜”类型。

let id x = x

let id1 = id 1
let id2 = id "two"

但是在使用运算符时,似乎是第一次调用确定了该函数的签名。

在这里,mul被报告为int -> int -> int

let mul x y = x * y

let mul1 = mul 1 2
let mul2 = mul 1.1 2.2 // fails here

如果我重新排序,则mulfloat -> float -> float

let mul x y = x * y

let mul2 = mul 1.1 2.2
let mul1 = mul 1 2 // fails here

您能从类型检查实现的角度解释(最好是非学术性的)术语是什么规则,以及规则如何工作?它是否遍历函数以在每次引用时检查其类型?还是有其他方法?

2 个答案:

答案 0 :(得分:5)

首先请注意,如果我们将mul声明为内联函数,则不会发生这种情况:

let inline mul x y = x * y

let mul1 = mul 1 2  // works
let mul2 = mul 1.1 2.2 // also works

在这里,mul的推断类型如下:

x: ^a -> y: ^b ->  ^c
    when ( ^a or  ^b) : (static member ( * ) :  ^a *  ^b ->  ^c)

此类型表示参数xy可以具有任何类型(甚至不必是同一类型),只要其中至少一个具有名为{的静态成员即可。 {1}}采用与*x相同类型的参数。 y的返回类型将与mul成员的返回类型相同。

那么,当*不是内联时,为什么没有同样的行为呢?因为成员约束(即说类型必须具有特定成员的类型约束)仅允许用于内联函数-这也是为什么类型变量的前面有mul而不是通常的^的原因:表示我们正在处理的是另一种类型的,类型较少的类型变量。

那么为什么对非内联函数存在这种限制?由于.NET支持。类型约束(例如“ T实现接口I”)可以在.NET字节码中表示,因此可以在所有函数中使用。类型约束(例如“ T必须具有名为U的特定成员,且类型为X的X”)无法表达,因此在普通函数中是不允许的。由于内联函数在生成的.NET字节码中没有相应的方法,因此不需要在.NET字节码中表示它们的类型,因此限制并不适用于它们。

答案 1 :(得分:3)

F#类型推论的这一方面在学术上并不特别优雅,但在实践中效果很好。 F#类型推断的工作方式是,编译器最初将所有内容都视为类型变量(泛型类型)并收集对它们的约束。然后尝试解决这些限制。

例如,如果您拥有:

let callWithTen f = f 10   

然后,最初,编译器会分配类型,以使callWithTen的类型为'a,而f的类型为'b。它还收集以下约束:

  • 'a = 'a0 -> 'a1是因为callWithTen在语法上被定义为一个函数
  • 'a0 = 'b,因为变量f是函数的参数
  • 'b = 'b0 -> 'b1,因为变量f被用作函数
  • 'b0 = int是因为f的自变量是int
  • 'b1 = 'a1是因为调用f的结果是callWithTen的结果。

解决这些约束,然后编译器推断callWithTen具有类型(int -> 'b1) -> 'b1

当代码中包含+时,您将无法完全确定数字类型是什么。其他一些ML语言通过将+用作整数,将+.用作浮点数来解决此问题,但这非常丑陋,因此F#采用了另一种方法,该方法有些特殊。

据我所知,F#沿'a supports (+)的方向具有约束。因此,在您的情况下(以稍微简化的描述),发生的情况是add是一个函数'a0 -> 'a0 -> 'a0,其中'a0 supports (+)

在处理其余代码时,编译器还将收集约束'a0 = int(在第一次调用中)和'a0 = float(在第二次调用中)。它首先解决第一个问题,这很好(因为int支持+),但是随后由于第二个约束而失败,因为int != float并在那里报告错误。