我想了解有关类型推断的规则,因为我希望将其纳入自己的语言,因此我一直在研究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
如果我重新排序,则mul
是float -> float -> float
:
let mul x y = x * y
let mul2 = mul 1.1 2.2
let mul1 = mul 1 2 // fails here
您能从类型检查实现的角度解释(最好是非学术性的)术语是什么规则,以及规则如何工作?它是否遍历函数以在每次引用时检查其类型?还是有其他方法?
答案 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)
此类型表示参数x
和y
可以具有任何类型(甚至不必是同一类型),只要其中至少一个具有名为{的静态成员即可。 {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
并在那里报告错误。