8

在某些情况下,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”实际上是“左值”,但它们位于表达式的“右手边”,这让每个人都感到非常惊讶。我知道这更多地与个人喜好有关,但对于大多数人来说,在一种特定情况下,输出值放在表达式的右侧似乎很奇怪。

我不认为所有这些都是错误,我怀疑大多数这些东西实际上是功能。
我的问题是:这种晦涩的行为背后是否有任何理性?

4

2 回答 2

7

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

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

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

[<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出现在模式中而不是表达式中的事实。F# 中的语法定义告诉你这样的事情:

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

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

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

于 2013-04-15T14:14:54.410 回答
7

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

在这个例子中,

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";}

从上面 MSDN 页面中所述的规则可以清楚地看出这种行为。

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

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

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"}
于 2013-04-15T14:09:54.983 回答