记录F#中的变化

12

我希望有一种方法来定义相关记录。例如,

type Thing       = { field1: string; field2: float }
type ThingRecord = { field1: string; field2: float; id: int; created: DateTime }
或者
type UserProfile = { username: string; name: string; address: string }
type NewUserReq  = { username: string; name: string; address: string; password: string }
type UserRecord  = { username: string; name: string; address: string; encryptedPwd: string; salt: string }

同时提供一种从一种格式转换到另一种格式的方法,而无需编写太多样板代码。即使完整的第一个示例也将是:

type Thing =
  { field1: string
    field2: float }
  with
    member this.toThingRecord(id, created) =
      { field1 = this.field1
        field2 = this.field2
        id = id
        created = created } : ThingRecord
and ThingRecord =
  { field1: string
    field2: float
    id: int
    created: DateTime }
  with
    member this.toThing() =
      { field1 = this.field1
        field2 = this.field2 } : Thing

当你开始使用field10等字段时,它会成为一种负担。

目前我使用反射以一种不安全(且缓慢)的方式执行此操作。

我在uservoice上为记录定义添加了一个请求,希望将with语法扩展到记录定义中,以满足这个需求。

但是也许已经有一种类型安全的方法可以做到这一点了吗?也许可以使用类型提供程序?

2个回答

6

是的,这是F#闪亮铠甲上的一个缺口。我觉得很难有一种通用的解决方案来轻松地继承或扩展记录。毫无疑问,人们渴望这样的解决方案 - 我已经数了超过十二个用户声音提交,呼吁改进这些方面 - 这里有一些领先的,可以自由投票: 1, 2, 3, 4, 5

当然,你可以做一些事情来解决这个问题,取决于你的情况,它们可能非常适合你。但最终 - 它们只是权宜之计,而且你必须牺牲一些东西:
  • 使用反射时会牺牲速度和类型安全性
  • 如果使用完整记录并在它们之间进行转换,则会缩短长度
  • 如果退回到普通.NET类和继承,则无法获得记录赋予您的所有语法和语义优势。
类型提供程序并不可行,因为它们不是元编程的好工具。那不是它们的设计目的。如果你试图以这种方式使用它们,你注定会遇到一些限制。
首先,你只能基于外部信息提供类型。这意味着,虽然你可以有一个类型提供程序,通过反射从.NET程序集中获取类型并提供一些派生类型,但你不能“内省”正在构建的程序集。因此,无法从同一程序集中早期定义的类型派生出新类型。
我猜你可以通过围绕类型提供程序构建项目来解决这个问题,但那听起来很笨重。即使如此,你也无法提供记录类型 yet ,所以你最好只能使用普通的.NET类。
对于提供数据库的某种ORM映射的更具体用例-我想你完全可以使用类型提供程序。只是不能作为通用元编程工具。

真遗憾,如果没有现成的解决方案,我计划下周做这件事,但是类型提供程序不支持记录类型这一令人惊讶的事实,有点破坏了这个想法。 - Dax Fohl
如果你有两个星期的时间,你可以尝试让类型提供程序发出记录类型 ;) - scrwtp
完全愿意,但请给我几天时间:我应该从哪里开始? - Dax Fohl
你需要反向工程F#编译器生成记录类型的过程,并进行相同的操作。最后一部分可能会比较棘手 - 如果它意味着扩展类型提供程序中可用的API。不确定是否可以在提供的类型上放置任意属性。 - scrwtp
我自己从未编写过足够复杂的类型提供程序(是的,我曾经考虑过使用类型提供程序进行元编程)。 - scrwtp

1
为什么不把它们嵌套得更深,像下面这样?
type Thing       = { Field1: string; Field2: float }
type ThingRecord = { Thing : Thing; Id: int; Created: DateTime }

或者

type UserProfile = { Username: string; Name: string; Address: string }
type NewUserReq  = { UserProfile: UserProfile; Password: string }
type UserRecord  = { UserProfile: UserProfile; EncryptedPwd: string; Salt: string }

转换函数很简单:
let toThingRecord id created thing = { Thing = thing; Id = id; Created = created }
let toThing thingRecord = thingRecord.Thing

使用方法:

> let tr = { Field1 = "Foo"; Field2 = 42. } |> toThingRecord 1337 (DateTime (2016, 6, 24));;

val tr : ThingRecord = {Thing = {Field1 = "Foo";
                                 Field2 = 42.0;};
                        Id = 1337;
                        Created = 24.06.2016 00:00:00;}
> tr |> toThing;;
val it : Thing = {Field1 = "Foo";
                  Field2 = 42.0;}

理想情况下是这样的,但数据库不支持,即使在理想情况下,有时候当你真正表示一个平面事物时,嵌套结构会感觉很奇怪。 - Dax Fohl
有时候会有交叉的东西,使得嵌套成为不可能。例如,当嵌套时,“WithId<WithTimestamp<Thing>>”将与“<WithTimestamp<WithId<Thing>>”不同;但我希望“thing.withId(id).withTimestamp(ts)”等价于“thing.withTimestamp(ts).withId(id)”。 - Dax Fohl

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接