F#记录的奇怪行为

时间:2013-04-15 13:13:12

标签: f#

有些情况下,F#记录行为对我来说很奇怪:

没有歧义警告

type AnotherPerson = {Id: int; Name: string}
type Person = {Id: int; Name: string;}

// F# compiler will use second type without any complains or warnings
let p = {Id = 42; Name = "Foo";}

警告记录解构而不是记录构建

在前一种情况下,F#编译器没有收到有关记录构造的警告,而是对记录“解构”发出警告:

// Using Person and AnotherPerson types and "p" from the previous example!
// We'll get a warning here: "The field labels and expected type of this 
// record expression or pattern do not uniquely determine a corresponding record type"
let {Id = id; Name = name} = p

请注意,没有模式匹配的警告(我怀疑这是因为模式是使用“记录构造表达式”而不是“记录解构表达式”构建的):

match p with
| {Id = _; Name = "Foo"} -> printfn "case 1"
| {Id = 42; Name = _} -> printfn "case 2"
| _ -> printfn "case 3"

缺少字段的类型推断错误

F#编译器会选择第二种类型而不会发出错误,因为缺少Age字段!

type AnotherPerson = {Id: int; Name: string}
type Person = {Id: int; Name: string; Age: int}

// Error: "No assignment given for field 'Age' of type 'Person'"
let p = {Id = 42; Name = "Foo";}

“记录解构”的丑陋语法

我向我的几个同事问了一个问题:“这段代码到底是什么意思?”

type Person = {Id: int; Name: string;}
let p = {Id = 42; Name = "Foo";}

// What will happend here?
let {Id = id; Name = name} = p

对于每个人来说,“id”和“name”实际上是“左倾”,尽管他们放置在表达的“右手边”,但这完全令人惊讶。我知道这更多是关于个人偏好,但对于大多数人来说似乎很奇怪,在一个特定情况下,输出值放在表达式的右侧。

我不认为所有这些都是错误,我怀疑这些东西大部分都是功能 我的问题是:这种模糊行为背后是否有任何理性?

2 个答案:

答案 0 :(得分:7)

您的示例可分为两类:记录表达式记录模式。虽然记录表达式需要声明所有字段并返回一些表达式,但记录模式具有可选字段并且用于模式匹配。 The MSDN page on Records有两个明确的部分,值得一读。

在此示例中,

type AnotherPerson = {Id: int; Name: string}
type Person = {Id: int; Name: string;}

// F# compiler will use second type without any complains or warnings
let p = {Id = 42; Name = "Foo";}

the MSDN page above中所述的规则明确了该行为。

  

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

在模式匹配的情况下,您专注于创建所需的一些绑定。所以你可以写

type Person = {Id: int; Name: string;}
let {Id = id} = p

以获得id绑定以供日后使用。 let绑定上的模式匹配可能看起来有点奇怪,但它与函数参数中通常模式匹配的方式非常相似:

type Person = {Id: int; Name: string;}
let extractName {Name = name} = name

我认为模式匹配示例的警告是合理的,因为编译器无法猜测您的意图。

然而,不建议使用具有重复字段的不同记录。至少你应该使用合格的名字来避免混淆:

type AnotherPerson = {Id: int; Name: string}
type Person = {Id: int; Name: string; Age: int}

let p = {AnotherPerson.Id = 42; Name = "Foo"}

答案 1 :(得分:5)

我认为您的大多数评论都与记录名称在定义记录的命名空间中直接可用的事实有关 - 也就是说,当您定义具有属性Person的记录NameId,名称NameId全局可见。这有利有弊:

  • 好处是它使编程更容易,因为你可以写{Id=1; Name="bob"}
  • 糟糕的是,名称可能与范围内的其他记录名称发生冲突,所以如果你的名字不是唯一的(你的第一个例子),你会遇到麻烦。

您可以告诉编译器您希望始终使用RequireQualifiedAccess属性明确限定名称。这意味着您将无法只编写IdName,但您需要始终包含类型名称:

[<RequireQualifiedAccess>]
type AnotherPerson = {Id: int; Name: string}
[<RequireQualifiedAccess>]
type Person = {Id: int; Name: string;}

// You have to use `Person.Id` or `AnotherPerson.Id` to determine the record
let p = {Person.Id = 42; Person.Name = "Foo" }

这为您提供了更严格的模式,但它使编程变得不那么方便。 @pad已经解释了默认值(更加模糊的行为) - 编译器只会选择稍后在源代码中定义的名称。即使在它可以通过查看表达式中的其他字段来推断类型的情况下也是如此 - 仅仅因为查看其他字段并不总是有效(例如,当您使用with关键字时),所以它更好坚持一个简单的一致策略。

至于模式匹配,当我第一次看到语法时,我感到非常困惑。我认为它不经常使用,但它可能很有用。

重要的是要意识到F#不使用结构类型(这意味着你不能使用带有更多字段的记录作为一个带有较少字段记录的函数的参数)。这可能是一个有用的功能,但它不适合.NET类型系统。这基本上意味着你不能指望过于花哨的东西 - 参数必须是一个众所周知的命名记录类型的记录。

当你写:

let {Id = id; Name = name} = p

术语左值指的是idname出现在模式而不是表达式 EM>。 F#中的语法定义告诉你这样的事情:

expr := let <pat> = <expr>
      | { id = <expr>; ... }
      | <lots of other expressions>

pat  := id
      | { id = <pat>; ... }
      | <lots of other patterns>

因此,=let的左侧是一个模式,而右侧是一个表达式。两者在F#中具有相似的结构 - (x, y)可用于构造和解构元组。记录的情况也一样......