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

Max*_*son 9 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!!!!
Run Code Online (Sandbox Code Playgroud)

为什么nextId类型为'a - > int而不是obj - > int?显然,泛化也是导致其反复返回1的错误的原因,但为什么泛化首先发生呢?

请注意,如果我在没有命名嵌套函数的情况下定义它,它会在给出唯一ID时按预期工作:

let nextId =
  let mutable i = 0
  fun (key:obj) ->
    i <- i + 1
    i
nextId "a" // returns 1
nextId "b" // returns 2
Run Code Online (Sandbox Code Playgroud)

但更神秘的是,根据这个定义,F#Interactive无法决定nextId是(obj - > int)还是('a - > int).当我第一次定义它时,我得到了

val nextId:(obj - > int)

但如果我只是评估

nextId
Run Code Online (Sandbox Code Playgroud)

我明白了

val it:('a - > int)

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

Tom*_*cek 8

我同意这是非常意外的行为.我认为F#执行泛化的原因是它将help(当返回时)视为fun x -> help x.调用一个函数obj似乎是编译器执行泛化的一种情况(因为它知道任何东西都可以obj).例如,在以下情况下会发生相同的概括:

let foo (o:obj) = 1
let g = fun z -> foo z
Run Code Online (Sandbox Code Playgroud)

在这里,g变得'a -> int太,就像在你的第一个版本.我不太清楚为什么编译器会这样做,但你看到的可以通过1)处理helpas fun x -> help x和2)推广调用来解释obj.

另一件事是F#如何处理泛型值 - 泛型值在ML语言中通常是有问题的(这就是整个"价值限制"业务的含义),但F#允许它在某些有限的情况下 - 例如你可以写:

let empty = []
Run Code Online (Sandbox Code Playgroud)

这定义了类型的通用值'a list.需要注意的是,这会被编译为每次访问该empty值时调用的函数.我认为你的第一个nextId函数以相同的方式编译 - 所以每次访问它时都会对body进行评估.

这可能不能回答原因,但我希望它提供了一些关于如何发生这种情况的提示 - 以及在其他情况下你所看到的行为可能是明智的!


scr*_*wtp 5

我不知道为什么编译器决定在你的第一个场景中进行推广,但最终nextId类型obj -> intvs 之间的区别'a -> int是驱动看似奇怪的行为的原因.

对于它的价值,您可以使用另一种类型注释"强制"第一个场景中的预期行为:

let nextId : obj -> int =
    let mutable i = 0
    let help (key:obj) =
        i <- i + 1
        i
    help
Run Code Online (Sandbox Code Playgroud)

现在,如果你将这些值放在模块中(比如在这个要点中),编译并检查ILSpy中的程序集,你会发现除了实例化计数器的ref单元格之外,代码几乎相同:

  • 在具体的情况下,nextId是一个产生一个函数的属性,该函数与模块的静态初始化器中的ref cell一起实例化,即所有调用nextId共享同一个计数器,

  • 在通用情况下,nextId是一个产生函数的泛型函数,并且ref单元格在其体内实例化,即每次调用时都有一个计数器nextId.

因此,通用案例中发出的代码实际上可以使用此代码段在F#中呈现:

let nextId () =
    let mutable i = 0
    fun key ->
        i <- i + 1
        i
Run Code Online (Sandbox Code Playgroud)

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