提问人:Lyndon Gingerich 提问时间:12/6/2022 更新时间:12/8/2022 访问量:194
通常使用函数将字段添加到匿名记录中
Generically add field to anonymous record using a function
问:
我希望能够将任意记录作为参数,并返回匿名记录,其中包含使用复制和更新语法添加的字段。
例如,这适用于:
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# 类型系统?
类型应类似于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 机制也不存在。
允许“是记录”约束应该不会那么复杂,但它不会解决任何问题。
评论
{| Foo: int |} -> {| Bar: int; Foo: int |}
inline
这目前是不可能的。您可以在此提案中找到有关它的讨论:
#807: 允许记录成为通用约束
如果我理解正确的话,他们在提案中提到的解决方案是,您必须能够将输入类型限制为记录。这是目前不可能的。
但正如 Gus 在他的回答中指出的那样,第二个考虑因素是,您还需要有一种方法来指定输出类型,因为输出类型取决于输入类型。
更新:
关于与提案相关的内容是否相关,或者我从中得出的结论是否正确,正在进行一些讨论。首先,我不在编译器上工作,所以我不知道最大的障碍是什么。但下面的分析在我看来并非没有道理。
首先,让我们摆脱泛型,感受一下正在发生的事情:
type Foo =
{ Foo: int }
let addBaz (x: Foo) = // Foo -> {| Baz: unit; Foo: int|}
{| x with Baz = () |}
这按预期进行编译和工作,并且几乎可以按照要求执行操作,但没有输入参数是通用的。
需要注意的几点:
- 无需显式定义输出类型。
- 不需要动态类型系统。
因为编译器可以从代码中推断类型定义:包含输入类型中所有字段的匿名记录和具有单位类型的附加字段(如果已经有 Baz 字段,这也将起作用,在这种情况下,它将被替换)。Foo
Baz
Foo
如果我们将其设为通用,则会出现以下编译错误
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# 中则不然。
评论
allow the conversion and extension of any anonymous record by adding a foo field
评论