How to derive record types in F#?

Viewed 64

I'm inserting data into Azure CosmosDB via FSharp.ComosDb. Here is the record type that I write in the DB:

[<CLIMutable>]
type DbType =
    { id: Guid
      Question: string
      Answer: int }

The persistence layer works fine but I face an inelegant redundancy. The record I'm inserting originates from the Data Transfer Object (DTO) with the following shape:

type DataType =
    { QuestionId: Guid
      Question: string
      Answer: int }

CosmosDb accepts only records with a lowercase id. Is there any way to derive the DbType from DataType or I have to define DbType from scratch?

Is there anything a la copy and update record expression record2 = { record1 with id = record1.QuestionId } but at the type level?

1 Answers

There's no type-level way of deriving one record type from another the way you describe, you can however get reasonably close with the addition of anonymous records in F# 4.6.

type DataType =
    { QuestionId: Guid
      Question: string
      Answer: int }

let example =
    { QuestionId = Guid.NewGuid()
      Question = "The meaning of life etc."
      Answer = 42 }

let extended =
    {| example with id = example.QuestionId |}

This gives you a value of an anonymous record type with an added field, and may be well suited to your scenario, however it's unwieldy to write code against such type once it leaves the scope of the function you create it in.

If all you care is how this single field is named - serialization libraries usually have ways of providing aliases for field names (like Newtonsoft.Json's JsonProperty attribute). Note that this might be obscured from you by the CosmosDb library you're using, which I'm not familiar with.

Another more involved approach is to use generic envelope types so that the records you persist have a uniform data store specific header across your application:

   type Envelope<'record> = 
      {
         id: string
         // <other fields as needed>
         payload: 'record
      }

In that case the envelope contains the fields that your datastore expects to be there to fulfill the contract (+ any application specific metadata you might find useful, like timestamps, event types, versions, whatnot) and spares you the effort of defining data store specific versions of each type you want to persist.

Note that it is still a good idea in general to decouple the internal domain types from the representation you use for storage for maintainability reasons.

Related