通常使用函数将字段添加到匿名记录中

Generically add field to anonymous record using a function

提问人:Lyndon Gingerich 提问时间:12/6/2022 更新时间:12/8/2022 访问量:194

问:

我希望能够将任意记录作为参数,并返回匿名记录,其中包含使用复制和更新语法添加的字段。

例如,这适用于:

let fooBar =
  {| Foo = ()
     Bar = () |}

let fooBarBaz = {| fooBar with Baz = () |}

但我想这样做:

let fooBar =
  {| Foo = ()
     Bar = () |}

let inline addBaz a = {| a with Baz = () |} (* The input to a copy-and-update expression that creates an anonymous record must be either an anonymous record or a record *)

let fooBarBaz = addBaz fooBar

有没有办法在 F# 中做到这一点?

F# 匿名类型

评论

1赞 Abel 12/9/2022
不是当前答案的一部分,但在 .NET 中,您可以动态发出程序集并加载它们。因此,上述情况是可能的,只是不是静态的,也不是容易的。根据您的用例,还要考虑类型提供程序、代码生成或动态类型(类似于 C# 功能)。

答:

4赞 Gus 12/6/2022 #1

不,那是不可能的。

想想看,如果这个功能是可能的,那会是什么类型? 它是否“适合”现有的 F# 类型系统?

类型应类似于val addField: x: {| FieldName<1>: 't1; FieldName<2>: 't2; ... FieldName<n>: 'tn;|} -> {| FieldName<1>: 't1; FieldName<2>: 't2; ... FieldName<n>: 'tn; Baz: unit |}

显然,类似的东西在 F# 类型系统中是无法表示的。

更新

有人提到,添加记录约束将允许这样做,但这与现实相去甚远。记录约束只会将参数过滤为记录,但仍然存在的问题是类型系统如何表示函数采用类型并返回类似^T when ^T : record^U when ^U : ^T_butWithAnAdditionalField

此外,在 SRTP 方面,有一种方法可以通过添加约束来“读取”字段,但不能写入,此外,读取字段的可能性允许我们读取已知名称的字段,而不是任何名称。get_FieldName

结论:F# 类型的系统离允许表达这样的东西还很遥远,而且 SRTP 机制也不存在。

允许“是记录”约束应该不会那么复杂,但它不会解决任何问题。

评论

0赞 Lyndon Gingerich 12/7/2022
你能澄清一下吗?例如,我可以制作一个功能 签名 .我想知道为什么我不能制作通用版本,也许使用 SRTP 限制字段。{| Foo: int |} -> {| Bar: int; Foo: int |}inline
0赞 Gus 12/7/2022
SRTP 签名会是什么样子?
0赞 Lyndon Gingerich 12/7/2022
正如 Tom 所说,我需要能够验证该参数是否为记录。我希望能够按字段名称限制参数类型,就像按成员名称一样。
0赞 Gus 12/7/2022
阅读我的评论。这还不够。您需要以泛型方式指定整个类型转换。
1赞 Tom Moers 12/8/2022
我现在明白你的意思了,我已经更新了我的答案
1赞 Tom Moers 12/6/2022 #2

这目前是不可能的。您可以在此提案中找到有关它的讨论:

#807: 允许记录成为通用约束

如果我理解正确的话,他们在提案中提到的解决方案是,您必须能够将输入类型限制为记录。这是目前不可能的。

但正如 Gus 在他的回答中指出的那样,第二个考虑因素是,您还需要有一种方法来指定输出类型,因为输出类型取决于输入类型。

更新:

关于与提案相关的内容是否相关,或者我从中得出的结论是否正确,正在进行一些讨论。首先,我不在编译器上工作,所以我不知道最大的障碍是什么。但下面的分析在我看来并非没有道理。

首先,让我们摆脱泛型,感受一下正在发生的事情:

type Foo = 
  { Foo: int }

let addBaz (x: Foo) = // Foo -> {| Baz: unit; Foo: int|}
  {| x with Baz = () |}

这按预期进行编译和工作,并且几乎可以按照要求执行操作,但没有输入参数是通用的

需要注意的几点:

  • 无需显式定义输出类型。
  • 不需要动态类型系统。

因为编译器可以从代码中推断类型定义:包含输入类型中所有字段的匿名记录和具有单位类型的附加字段(如果已经有 Baz 字段,这也将起作用,在这种情况下,它将被替换)。FooBazFoo

如果我们将其设为通用,则会出现以下编译错误

let addBaz (x: 'a) =
      {| x with Baz = () |}

FS3245:创建匿名记录的复制和更新表达式的输入必须是匿名记录或记录

这是有道理的,因为没有办法说 x 是记录。我们可以给它一个类,或者只是一个整数。

因此,假设我们可以将输入限制为记录,这在理论上是否可行?

让我们通过推断类型来检查一些情况,就像编译器在一些示例中所做的那样:

type Foo = 
  { Foo: int }

type BazInt =
  { Baz: int }

type BazUnit =
  { Baz: unit }

type MoreFields =
  { Bar: string
    Foo: int }

let addBaz (x: BazInt) =
  {| x with Baz = () |}

let test () =
  let foo` = addBaz { Foo = 5 } // OK: addBaz monomorphizes* to `Foo -> {| Baz: unit; Foo: int |}`
  let bazInt` = addBaz { BazInt.Baz = 5 } // OK: addBaz monomorphizes* to `BazInt -> {| Baz: unit |}`
  let bazUnit` = addBaz { BazUnit.Baz = () } // OK: addBaz monomorphizes* to `BazUnit -> {| Baz: unit |}`
  let moreFields` = addBaz { MoreFields.Foo = 1; MoreFields.Bar = "" } // OK: addBaz monomorphizes* to `MoreFields -> {| Bar: string; Baz: unit; Foo: int |}`

*:我使用单态化是因为这是我从 C++ 模板中知道的术语,我不确定 .net 是否使用相同的术语。但是 AFAIK,这个讨论无关紧要,想法是一样的:你只需将泛型输入类型替换为调用函数时使用的具体类型。'a

更新2:

我现在明白了 Gus 对返回类型所指的内容。问题不在于它不能像我所展示的那样推导出来。问题是您需要以某种方式显示泛型输出类型。这在 F# 中目前是不可能的。返回类型仅在调用函数时变为“complete”。在 C++ 中,这完全没问题,但在 F# 中则不然。

评论

1赞 Gus 12/7/2022
不,这是不正确的。该提案不允许按问题中指定的常规方式添加字段。最重要的是,您需要一种以通用方式指定字段名称的方法,目前不支持该方法,并且该提案未涵盖它。看看我的答案。
0赞 Tom Moers 12/7/2022
你可能不同意提案是正确的,但它非常清楚地说明了它的意图:。对我来说,这与所问的内容非常接近。allow the conversion and extension of any anonymous record by adding a foo field
0赞 Gus 12/7/2022
好吧,这不是提案的标题,也不是包含在您可以定义的内容的描述中。错误地指出,由于这种添加,它将允许添加字段,但如果您遵循讨论,很明显假设是错误的。
1赞 Gus 12/7/2022
另外,你说“主要问题是你必须能够将输入类型限制为记录”,对不起,但这不是主要问题。这相对容易添加,复杂的部分是“升级”类型系统,以便能够表示以通用方式将字段添加到记录的函数的类型。
1赞 Lyndon Gingerich 12/8/2022
@Gus 请不要删除您的答案,除非您觉得它完全多余。在我看来,它似乎增加了有用的信息。