为什么这会混淆 F# 编译器的类型推断?

Why does this confuse the F# compiler's type inference?

这里没问题:

module Seq =
    let private rnd = Random Environment.TickCount

    let random =
        fun (items : 'T seq) ->
            let count = Seq.length items
            items |> Seq.nth (rnd.Next count)

Seq.random的签名是items:seq<'T> -> 'T。一切顺利。

是的,我知道我可以 let random items = [...],这不是重点。

关键是当我这样做时 items 突然被限制为类型 seq<obj>:

module Seq =
    let random =
        let rnd = Random Environment.TickCount
        fun (items : 'T seq) ->
            let count = Seq.length items
            items |> Seq.nth (rnd.Next count)

... 即我将 Random 对象添加为闭包。如果我将鼠标悬停在 random 上,Intellisense 会显示签名已变为 items:seq<obj> -> obj.

有趣的是,如果我 select 代码并点击 [Alt]+[Enter] 以在 F# Interactive 中执行它,签名显示为 seq<'a> -> 'a。 WTH??

所以,这是怎么回事?为什么类型推断会出现混乱和不一致?

这就是所谓的Value Restriction。长话短说,语法值不能是通用的,因为当发生突变时它可能会破坏事物,并且编译器不能总是可靠地证明不变性。 (请注意,尽管 random 在语义上是一个函数,但在句法上它仍然是一个值 ,这才是最重要的)

但是有时编译器可以证明不变性。这就是您的第一个示例起作用的原因:当 let 的右侧是一个直接的 lambda 表达式时,编译器可以确定它是不可变的,因此它允许它通过。

另一个例子是 let x = [] - 这里编译器可以看到 nil 列表 [] 是不可变的。另一方面,let x = List.append [] [] 将不起作用,因为在这种情况下编译器无法证明不变性。

此 "relaxation" 值限制是在 F# 中根据具体情况进行的。 F# 编译器只能处理一些特殊情况:字面量、lambda 表达式等,但它通常没有成熟的机制来证明不变性。这就是为什么,一旦您走出这些特殊情况,就不允许拥有通用值。

从技术上讲,您可以通过添加显式类型参数来解决这个问题。从逻辑上讲,这告诉编译器 "Yes, I know it's a generic value, and that's what I meant for it to be".

let random<'t> : seq<'t> -> 't =
    let rnd = Random Environment.TickCount
    fun items ->
        let count = Seq.length items
        items |> Seq.nth (rnd.Next count)

let x = random [1;2;3]

但这仍然不会如你所愿,因为在幕后,这样的定义将被编译成一个无参数的通用方法,每次你引用这样的"value",该方法将被调用并return 你一个新功能 - 每次调用都有一个全新的 rnd 。换句话说,上面的代码将等同于:

let random() =
    let rnd = Random Environment.TickCount
    fun items ->
        let count = Seq.length items
        items |> Seq.nth (rnd.Next count)

let x = random() [1;2;3]