Is it possible to write a generic based on a record with a named field?

I have some code which has a lot of repetition.

type RecordA = {
  Name: string
  // ...
}

type RecordB = {
  Name: string
  // ...
}

val getTheHandler: (name: string) -> (() -> ())

let handleA (record: RecordA) =
  (getTheHandler record.Name) ()


let handleB (record: RecordB) =
  (getTheHandler record.Name) ()

I'm wondering if it is possible to write some generic function that would let me simplify/refactor the getTheHandler record.Name. In trying to refactor that snippet the compiler wants to choose one record type of the other.

So trying this, I get a compiler error:

let shorter (record: 'T) =
   (getTheHandler record.Name) ()

// later:
shorter myRecordA // FS0001: This expression was expected to have type RecordB but here has type RecordA
  

Is this possible? Is the only way to make this work to add a member function to each record type?

2 answers

  • answered 2021-10-12 16:07 Phillip Carter

    Yes, it is possible with SRTP - see here: Partial anonymous record in F#

    Here's an example using your use case:

    let getTheHandler (name: string) () = printfn $"{name}"
    
    let inline handle (r: ^T) =
        (^T : (member Name: string) r)
    
    let ra: RecordA = { Name = "hello" }
    let rb: RecordB = { Name = "world" }
    
    handle ra // "hello"
    handle rb // "world"
    

    I'd also say though, a little repetition isn't that bad. SRTPs can be wonderful, but can lead down a path of getting way too happy with abstraction and sometimes compile-time slow downs. Using it judiciously like this isn't that bad though.

    Mods, although the linked Q&A is earlier than this one, this question name is a lot more relevant to the solution, so if possible I think it'd be better to keep this one up.

  • answered 2021-10-12 22:11 Tomas Petricek

    Just for the record, you can also solve this problem by using ordinary object-oriented interfaces. This is something that works quite well with functional design in F# and it is quite clean. There is some more work involved in explicitly implementing the interfaces, but the up side is that you end up with more clear explicit code (and the interface can model the intention better than just a member name):

    To define and implement an interface:

    type INamed = 
      abstract Name : string
    
    type RecordA = 
      { Name: string }
      interface INamed with 
        member x.Name = x.Name
    
    type RecordB = 
      { Name: string }
      interface INamed with 
        member x.Name = x.Name
    

    To use this:

    let getTheHandler (name:string) = 
      fun () -> printfn "Hi %s" name
    
    let handle (record: INamed) =
      (getTheHandler record.Name) ()
    

How many English words
do you know?
Test your English vocabulary size, and measure
how many words do you know
Online Test
Powered by Examplum