我最近遇到了来自F#编译器的一些意外行为。我找到了一个解决方法,但最初的行为令我感到困惑,我想知道是否有人可以帮助我理解是什么导致它。
我定义为非泛型的函数变得通用,这干扰了函数在多个调用之间共享状态的能力。我将用例简化为以下内容:
let nextId =
let mutable i = 0
let help (key:obj) =
i <- i + 1
i
help
nextId "a" // returns 1
nextId "b" // also returns 1!!!!
为什么 nextId 类型'a - &gt; int而不是obj - &gt;诠释?很明显,泛化也是导致它反复返回1的错误的原因,但为什么泛化首先发生呢?
请注意,如果我在没有命名嵌套函数的情况下定义它,它会在给出唯一ID时按预期工作:
let nextId =
let mutable i = 0
fun (key:obj) ->
i <- i + 1
i
nextId "a" // returns 1
nextId "b" // returns 2
但更神秘的是,根据这个定义,F#Interactive无法决定nextId是(obj - > int)还是('a - &gt; int)。当我第一次定义它时,我得到了
val nextId:(obj - &gt; int)
但如果我只是评估
nextId
我得到了
val it :('a - &gt; int)
这里发生了什么,为什么我的简单函数会自动推广?
答案 0 :(得分:8)
我同意这是非常意想不到的行为。我认为F#执行泛化的原因是它将help
(在返回时)视为fun x -> help x
。调用一个带obj
的函数似乎是编译器执行泛化的一种情况(因为它知道任何东西都可以是obj
)。例如,在:
let foo (o:obj) = 1
let g = fun z -> foo z
此处,g
也变为'a -> int
,就像您的第一个版本一样。我不太清楚为什么编译器会这样做,但是你看到的内容可以解释为1)将help
视为fun x -> help x
和2)对使用obj
的调用进行推广。
正在发生的另一件事是F#如何处理泛型值 - 泛型值在ML语言中通常是有问题的(这就是整个“价值限制”业务的意义),但F#允许它在某些有限的情况下 - 你可以为示例写道:
let empty = []
这定义了类型'a list
的通用值。需要注意的是,这会被编译为每次访问empty
值时调用的函数。我认为您的第一个nextId
函数以相同的方式编译 - 因此每次访问时都会对主体进行评估。
这可能不会回答为什么,但我希望它提供一些关于如何发生这种情况的提示 - 以及在其他情况下你所看到的行为可能是明智的!
答案 1 :(得分:5)
我不知道为什么编译器决定在你的第一个场景中进行推广,但最终nextId
类型obj -> int
与'a -> int
之间的区别是驱动看似奇怪的行为的原因这里。
对于它的价值,您可以使用另一种类型注释“强制”第一个场景中的预期行为:
let nextId : obj -> int =
let mutable i = 0
let help (key:obj) =
i <- i + 1
i
help
现在,如果你将这些值放在模块中(比如这个gist),编译并检查ILSpy中的程序集,你会发现代码几乎相同,除了计数器的ref单元格是实例:
在具体情况下,nextId
是一个产生函数的属性,该函数与模块的静态初始化器中的ref cell一起实例化,即对nextId
的所有调用共享相同的计数器,
在一般情况下,nextId
是一个产生函数的泛型函数,ref单元格在其体内实例化,即每次调用nextId
时都有一个计数器。
因此,通用案例中发出的代码实际上可以使用此代码段在F#中呈现:
let nextId () =
let mutable i = 0
fun key ->
i <- i + 1
i
最重要的是,当你有这样的通用值时,发出编译器警告是有意义的。一旦你知道问题就很容易避免这个问题,但这是你不会看到的其中一个问题。