8

在以下代码片段中

type myrec1 = {x: int; y: int}
type myrec2 = {x: int; y: int; z: int}

let p1 = {x = 1; y = 1}  // Error. p1 compiler assumes p1 has the type myrec2

// It works with additional type specification
let p1: myrec1 = {x = 1; y = 1}
let p2: myrec2 = {x = 1; y = 1; z = 1}

带有注释的行无法编译。由于某种原因,类型检查器无法确定 p1 的类型应该是 myrec1。是因为这种类型推断的情况根本无法解决,还是只是 F# 类型推断的限制?

4

4 回答 4

9

这里

最近声明的类型的标签优先于先前声明的类型的标签

所以如果你这样做:

type myrec2 = {x: int; y: int; z: int}
type myrec1 = {x: int; y: int}

let p1 = {x = 1; y = 1}

那么它将起作用。

为了您的阅读乐趣(F# 3.0 规范):

field-initializer : long-ident = expr

6.3.5 记录表达式

在这种情况下,我们的字段初始化器不是单个标识符,因此它使用 14.1.9 中的“字段标签解析”。

每个 field-initializeri 的格式为 field-labeli = expri。每个字段标签都是一个长标识,它必须解析为唯一记录类型 R 中的字段 Fi,如下所示:

· 如果 field-labeli 是单个标识符 fld 并且已知初始类型是记录类型 R< ,..., > 具有名称为 fld 的字段 Fi,则字段标签解析为 Fi。

· 如果 field-labeli 不是单个标识符或者如果初始类型是变量类型,则通过对 field-labeli 执行字段标签解析(参见第 14.1 节)来解析字段标签。此过程产生一组字段 FSeti。该集合的每个元素都有一个对应的记录类型,从而产生一组记录类型 RSeti。所有 RSeti 的交集必须产生单个记录类型 R,然后每个字段解析为 R 中的相应字段。

14.1.9 字段标签解析

我们的 long-ident 是一个 FieldLabel,所以它是使用 8.4.2 中描述的 FieldLabels 表查找的。

字段标签解析指定如何解析 { field1 = expr; 中的 field1 等标识符 ...字段N = expr }。字段标签解析通过以下步骤进行:

1. 在 Types 表和 FieldLabels 表(第 8.4.2 节)中查找所有可用类型中的所有字段。

2. 返回字段声明集。

8.4.2 名称解析和记录字段标签

如此处所述,FieldLabels 表用于成员名称解析 (14.1)。

对于记录类型,除非记录类型具有 RequireQualifiedAccess 属性,否则记录字段标签 field1 ... fieldN 将添加到当前名称解析环境的 FieldLabels 表中。FieldLabels 表中的记录字段标签在成员名称解析(第 14.1 节)中发挥特殊作用:可以从记录标签中推断出表达式的类型。例如:类型 R = { dx : int; dy: int } let fx = x.dx // x 被推断为类型 R 在本例中,查找 .dx 被解析为字段查找。

14.1.4 表达式中 的名称解析 这部分看起来有点模糊,但我认为它在这一点上使用了名称解析。如最后所述,如果有多个,则返回第一个项目。

给定输入 long-ident、环境 env 和后续类型参数 < ,..., > 数量的可选计数 n,表达式中的名称解析计算包含 long-ident< ,..的解释的结果。 ., > 作为值或其他表达式项的前缀,以及残差路径。表达式中的名称解析如何进行取决于 long-ident 是单个标识符还是由多个标识符组成。如果 long-ident 是单个标识符 ident:

1. 在 ExprItems 表中查找 ident。返回结果并清空其余部分。

2. 如果 ident 没有出现在 ExprItems 表中,则在 Types 表中查找它,如果可用,则使用与 n 匹配的通用 arity。返回此类型并清空其余部分。

3. 如果 ident 没有出现在 ExprItems 表或 Types 表中,则失败。

...

如果表达式包含歧义,表达式中的名称解析将返回该过程生成的第一个结果。

您感兴趣的部分是上面的最后一行:“返回过程生成的第一个结果”。

于 2013-04-04T13:46:20.847 回答
4

此行为是设计使然。我不能说这是 F# 类型推断的限制,还是一般类型推断算法的限制;如果您考虑一下,有两种选择:

  1. 给定一个记录表达式{x = 1; y = 1},将“x”和“y”字段与最后一个类型匹配以声明它们中的任何一个。这是最容易理解的,也是 F# 编译器实现记录类型推断的方式。

  2. 尝试根据当前范围内类型的字段确定“最佳匹配”。(我想这就是你要问的。)

    但是,这种算法可能会导致其他问题;具体来说,对于记录表达式{x = 1; y = 1},编译器无法判断您是否打算创建 type 的表达式myrec1,或者您是否打算创建表达式myrec2并且您忘记为该z字段分配值。

    另外,如果你声明了两个具有完全相同字段的类型,编译器应该怎么做?例如,如果您添加:

    type myrec3 = {x: int; y: int}
    

换句话说,没有免费的午餐——你可以增加类型推断的“力量”,但它会降低你从编译器获得的错误诊断的准确性。

于 2013-04-04T13:46:54.347 回答
4

如果您想要具有类似名称字段的记录类型,区分它们的方法是:

let p1 = {myrec1.x = 1; y = 1}
于 2013-04-04T14:05:12.343 回答
1

由于没有类型注释,编译器从标签推断记录类型,但第一种类型的标签与第二种标签的区别不够大,因此后者优先,p1 被推断为 myrec2 类型。使用不同的标签以避免类型注释并获得预期的类型推断行为:

type myrec1 = {x1: int; y: int}
type myrec2 = {x2: int; y: int; z: int}

let p1 = {x1 = 1; y = 1}
于 2013-04-04T13:47:56.873 回答