为什么F#编译器有时会错误地推广函数?

时间:2017-07-03 20:24:23

标签: generics f# automatic-generalization

我最近遇到了来自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)

这里发生了什么,为什么我的简单函数会自动推广?

2 个答案:

答案 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

最重要的是,当你有这样的通用值时,发出编译器警告是有意义的。一旦你知道问题就很容易避免这个问题,但这是你不会看到的其中一个问题。