9

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

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

val nextId : (obj -> int)

但如果我只是评估

nextId

我明白了

验证它:('a -> int)

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

4

2 回答 2

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 = []

这定义了 type 的通用值'a list。需要注意的是,这会被编译为每次访问该empty值时都会调用的函数。我认为您的第一个nextId函数以相同的方式编译 - 因此每次访问它时都会评估主体。

这可能无法回答原因,但我希望它提供更多关于这是如何发生的提示 - 在其他情况下,您看到的行为可能是明智的!

于 2017-07-03T20:55:05.110 回答
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 单元一起实例化,即所有调用nextId共享相同的计数器,

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

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

let nextId () =
    let mutable i = 0
    fun key ->
        i <- i + 1
        i

最重要的是,当您有这样的通用值时,发出编译器警告是有意义的。一旦你知道它在那里,就很容易避免这个问题,但这是你不会看到的那些事情之一。

于 2017-07-03T22:32:08.843 回答