Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Record variations in F#

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?

like image 710
Dax Fohl Avatar asked Jun 24 '16 15:06

Dax Fohl


2 Answers

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:

  • Speed and type safety when using reflection,
  • Brevity when you go the type safe way and have full-fledged records with conversion functions between them,
  • All the syntactic and semantic goodness that records give you for free when you decide to fall back to plain .NET classes and inheritance.

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.

like image 69
scrwtp Avatar answered Nov 16 '22 04:11

scrwtp


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;}
like image 44
Mark Seemann Avatar answered Nov 16 '22 02:11

Mark Seemann