Record update syntax for types and update to another type?

In code interacting with a database, I have a record type for an entry in a table, for example

 type RelationSpec =
    { id : RelationId option
      parentSideType: RelationSideType
      childSideType: RelationSideType
      parentToChildName: string
      childToParentName: string
      parentId: EntityId
      childId: EntityId
    }

The option field is leading to sub optimal code I think.
With this code for example,

let handleRequest(spec:RelationSpec) =
  match spec.id with
  | Some id -> updateEntry spec
  | None -> createEntry spec

I end up with an identical match in updateEntry. So what I do now is pass the id as additional argument to updateEntry.

What I really would like to do is something like this, which, has far as I’m aware, is not possible:

 type NewRelationSpec =
    {
      parentSideType: RelationSideType
      childSideType: RelationSideType
      parentToChildName: string
      childToParentName: string
      parentId: EntityId
      childId: EntityId
    }
 // would be cool to have the same approach as for updating records
 type ExistingRelationSpec =
    { NewRelationSpec with
        id: RelationId
    }

but then createEntry would be

let createEntry(spec:NewRelationSpec):ExistingRelationSpec=
   let newId = insertInDB(spec)
   // next line doesn't work because record updates only work if new record is of same type.
   {spec with id=newId}

So I’m curious to read your answers: is this already possible? If not would it make sense as an addition to the language? Or is there a better existing approach?

So, you can sort of already do this (your syntax is just a bit off). Try something like:

type NewRelationSpec =
    {
        parentSideType: RelationSideType
        childSideType: RelationSideType
        parentToChildName: string
        childToParentName: string
        parentId: EntityId
        childId: EntityId
    }

type ExistingRelationSpec = 
    { 
        spec: NewRelationSpec
        id: RelationId
    }

// ... elsewhere ...

let createEntry (spec : NewRelationSpec) : ExistingRelationSpec =
   let newId = insertInDB(spec)
   // next line doesn't work because record updates only work if new record is of same type.
   { id=newId; spec=spec }

At least, I think that’ll give you what you want… Though it’s possible I’ve misunderstood your goals.

Thanks for your reaction! You understood my goal.
Actually, I took that exact approach, but I didn’t find it great (I forgot why exactly, I didn’t post my question immediately :frowning: ). In any case I regret losing the direct mapping from the record field to the database table field, and the convention of the subfield name in which to store common fields record will change from code base to code base. In short it makes it somewhat less approachable for new eyes I think.

Wouldn’t the hypothetical syntax in my question not be better if this is a common pattern in F# code? I have no idea about its feasability though :slight_smile:

Wouldn’t the hypothetical syntax in my question not be better if this is a common pattern in F# code? I have no idea about its feasability though

This kind of thing is somewhat common, and in general there are some desires from people to simplify the current verbose way to handle records.

However, the verbosity involved with records (at least in terms of manipulating them) is kind of by design. Records are primarily intended to be constructed once and have that data flow through an application. And you want construction to typically be as clear and explicit as possible. There are [several suggestions involving records](Issues · fsharp/fslang-suggestions · GitHub that are worth looking at.

1 Like

A specific situation where you can do that is using Anonymous Records:

type RelationId = RelationId
type RelationSideType = RelationSideType
type EntityId = EntityId

type NewRelationSpec =
   {|
        parentSideType: RelationSideType
        childSideType: RelationSideType
        parentToChildName: string
        childToParentName: string
        parentId: EntityId
        childId: EntityId
   |}

type ExistingRelationSpec =
   {|
        parentSideType: RelationSideType
        childSideType: RelationSideType
        parentToChildName: string
        childToParentName: string
        parentId: EntityId
        childId: EntityId
        id: RelationId
   |}

let insertInDB spec = RelationId

let createEntry(spec:NewRelationSpec) : ExistingRelationSpec=
   let newId = insertInDB(spec)
   // next line does work now, because anon records allow this
   {| spec with id=newId |}

That looks very interesting! Do you have experience using this? Any specific downsides of anonymous records that you encountered with this? I don’t think I would encounter big problems with the limitations of anonymous records in my code, but I might miss something.

Thanks for pointing this possibility with anonymous records!

I used them a couple of times and they work pretty well.
Type inference works in a different way, but I don’t think that would affect you in your scenario.
The only limitation of this approach is that the destination type has to have always the same field as the original one, which is the case in your example. If at anytime a field disappears, you will have to type the full expression, copying all fields.

1 Like