I'd like some way to define related records. For example,
type Thing = { field1: string; field2: float }
type ThingRecord = { field1: string; field2: float; id: int; created: DateTime }
or
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 }
along with a way to convert from one to the other, without needing to write so much boilerplate. Even the first example in full would be:
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
As you get up to field10
etc, it gets to be a liability.
I currently do this in an unsafe (and slow) manner using reflection.
I added a request for with
syntax to be extended to record definitions on uservoice, which would fill this need.
But is there perhaps an typesafe way to do this already? Maybe with type providers?
Yes, that's a chink in F#'s otherwise shiny armor. I don't feel there's a universal solution there for easily inheriting or extending a record. No doubt there is an appetite for one - I've counted over a dozen uservoice submissions advocating improvements along these lines - here are a few leading ones, feel free to vote up: 1, 2, 3, 4, 5.
For sure, there are things you can do to work around the problem, and depending on your scenario they might work great for you. But ultimately - they're workarounds and there's something you have to sacrifice:
Type providers won't cut it because they're not really a good tool for metaprogramming. That's not what they were designed for. If you try to use them that way, you're bound to hit some limitation.
For one, you can only provide types based on external information. This means that while you could have a type provider that would pull in types from a .NET assembly via reflection and provide some derived types based on that, you can't "introspect" into the assembly you're building. So no way of deriving from a type defined earlier in the same assembly.
I guess you could work around that by structuring your projects around the type provider, but that sounds clunky. And even then, you can't provide record types anyway yet, so best you could do are plain .NET classes.
For a more specific use case of providing some kind of ORM mapping for a database - I imagine you could use type providers just fine. Just not as a generic metaprogramming facility.
Why don't you make them more nested, like the following?
type Thing = { Field1: string; Field2: float }
type ThingRecord = { Thing : Thing; Id: int; Created: DateTime }
or
type UserProfile = { Username: string; Name: string; Address: string }
type NewUserReq = { UserProfile: UserProfile; Password: string }
type UserRecord = { UserProfile: UserProfile; EncryptedPwd: string; Salt: string }
Conversion functions are trivial:
let toThingRecord id created thing = { Thing = thing; Id = id; Created = created }
let toThing thingRecord = thingRecord.Thing
Usage:
> 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;}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With