Search code examples
f#type-providers

F# WsdlService Provider: Reduce Repetitious Mapping Code in Multiple Providers


I have three WsdlService type providers each pointing to a different version of the same WCF service, and the underlying MyService.ServiceTypes.Ticket are mostly the same. I am using the WsdlService provider so that the only code we have in source is the objects we use from the service. This project has started to replace a slice of an old project where everything generated by SvcUtil.exe is checked in to source, even if it is unused (yuck).

I have a hunch that code quotations or higher kinded types might work because the functions I want to condense are so similar. My use case does not appear similar to examples I have seen using F# quotations, and I do not yet understand HKTs.

For just 3 service versions, the copy-paste approach in module VersionMappers is not so bad. However, we have about 10 different versions of this service. I can do copy-paste again, but this feels like something a functional language can handle.

My overarching questions: Is there some F# way to reduce the repetitious ticketxxToDomain code in module VersionMappers to something like I have in module CanThisPseudoCodeBeReal, or have I cornered myself in copy-paste land? Are code quotations or HKTs a viable option?

``` f#

module DataAccess =
  type Service40 = WsdlService<My40ServiceUrl>
  type Service41 = WsdlService<My41ServiceUrl>
  type Service42 = WsdlService<My42ServiceUrl>

// All the ticketXXToDomain functions look like they can be less copy-paste, but how?
// Still, I think this F# approach is MUCH better than the monolith project we have where WCF SvcUtil.exe
// generate everything for every version and we check that in to source.
module VersionMappers =
  open DataAccess
  module Ticket =
    let ticket40ToDomain (t : Service40.ServiceTypes.Ticket) =
      MyCSharpProject.Pocos.Ticket(
        TicketId = t.TicketId
        // with 10+ other fields
        TicketType = t.TicketType)
    let ticket41ToDomain (t : Service41.ServiceTypes.Ticket) =
      MyCSharpProject.Pocos.Ticket(
        TicketId = t.TicketId
        // with 10+ other fields
        TicketType = t.TicketType)
    let ticket42ToDomain (t : Service42.ServiceTypes.Ticket) =
      MyCSharpProject.Pocos.Ticket(
        TicketId = t.TicketId
        // with 10+ other fields
        TicketType = t.TicketType)

module CanThisPseudoCodeBeReal =
  type ServiceTicket =
    | V40Ticket of Service40.ServiceTypes.Ticket
    | V41Ticket of Service41.ServiceTypes.Ticket
    | V42Ticket of Service42.ServiceTypes.Ticket
  let ticketToDomain (t : 'a when 'a is one case of ServiceTicket) =
    // Now the compiler will hopefully complain if t.SomeField is not on
    // all cases, and I am happy to handle that.
    MyCSharpProject.Pocos.Ticket(
      // I am using fake field names for clarity.
      // every VxxTicket has this field, no warning
      TicketId = t.TicketId,
      // compiler warns that only V40Ticket has this field
      SomeV40Field = t.SomeV40Field,
      // compiler warns that V42Ticket does not have this field
      SomeV40And41Field = t.SomeV40And41Field

    )

```

Side note: everything surrounding the ServiceTypes.Ticket object, including the service calls to retrieve them, are identical across all versions of the service so we could just use one WsdlService pointing to the latest version. We confirmed this from the source code. There are other slices of the service where is not the case, so I am trying to see if there is some way to reduce the repetition before people complain about it. This project is a pilot to try out F# and the WsdlService provider to "strangle" part of the existing WCF service facade.


Solution

  • F# knows Statically Resolved Type Parameters which allow for 'compile-time generics' (typesafe duck-typing):

    open System
    
    type T1 = { f1: string; f2: int; f3: DateTime }
    type T2 = { f2: int; f3: DateTime; f4: double }
    
    type TCommon = { f2: int; f3: DateTime }
    
    let inline toTCommon n = {
        f2 = (^N : (member f2 : int) n)
        f3 = (^N : (member f3 : DateTime) n) }
    

    the signature of toTCommon is:

    val inline toTCommon :
      n: ^N -> TCommon
        when  ^N : (member get_f2 :  ^N -> int) and
              ^N : (member get_f3 :  ^N -> DateTime)
    

    Any type having f2 : int and f3 : DateTime satisfies this.