为什么这会混淆 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]
这里没问题:
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]