Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

f#

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?

like image 292
Flame Avatar asked Dec 06 '22 07:12

Flame


2 Answers

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.

like image 87
Phillip Carter Avatar answered Jun 16 '23 12:06

Phillip Carter


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) ()
like image 41
Tomas Petricek Avatar answered Jun 16 '23 12:06

Tomas Petricek