To Transfer Attributes Directly from One Type to Another without Composition

Jet balloons

Or, “REMOVE ALL THOUGHTS OF INHERITANCE! THIS IS FUNCTIONAL PROGRAMMING, DAMMIT!”

The context: While working with a set of API contract, we found multiple types that have a lot of common attributes. To reduce code duplication, we wanted to explore the usefulness of and ways to extract the common attributes into a single common type that could then be applied within other types.

Technically, this is what Composition is built for. The way it works is to create a single common type with all of the common attributes and compose this “common” type into the others.

Before composition After composition
open System
type A = { id: String; name: String; age: int}
type B = 
  { id: String; name: String; gender: string}
type C = 
  { id: String; name: String; height: float}
let a:A = { id = "123"; name = "ABC"; age = 54}
let b:B = 
  { id = "123"; name = "ABC"; gender = "Female"}
let c:C = 
  { id = "123"; name = "ABC"; height = 5.9}
let lookupA = a.id
let lookupB = b.id
let lookupC = c.id
open System
type Common = {id: string; name: string}
type A = {common: Common; age: int}
type B = {common: Common; gender: string}
type C = {common: Common; height: float}
let common:Common = {id = "123"; name = "ABC"}
let a:A = {common = common; age = 54}
let b:B = {common = common; gender = "Female"}
let c:C = {common = common; height = 5.9}
let lookup x = x.id
lookup a.common
lookup b.common
lookup c.common

This works great for our current model. Brilliant! But, trying to futureproof, we realize that the original Types might change in the next version. Maybe for Type A, we decide that the “id” attribute needs to be an int. Well, that’s easy enough. Just modify the above to the following:

V1 V2
open System
type Common = {id: string; name: string}
type A = {common: Common; age: int}
type B = {common: Common; gender: string}
type C = {common: Common; height: float}
let common:Common = {id = "123"; name = "ABC"}
let a:A = {common = common; age = 54}
let b:B = {common = common; gender = "Female"}
let c:C = {common = common; height = 5.9}
let lookup x = x.id
lookup a.common
lookup b.common
lookup c.common
open System
type Common = {id: string; name: string}
type A = {common: Common; id: int; age: int}
type B = {common: Common; gender: string}
type C = {common: Common; height: float}
let common:Common = {id = "123"; name = "ABC"}
let a:A = {common = common; age = 54; id = 123}
let b:B = {common = common; gender = "Female"}
let c:C = {common = common; height = 5.9}
let lookupA (y:A) = y.id
let lookup (x:Common) = x.id
lookupA a
lookup b.common
lookup c.common

Done and done! But, wait, we see that we need to create a specific type to lookup for A, as the default type would not work. Unfortunately, this undoes the point of using a common type in the first place. Further, in a complex version of this problem, like the one we’re actually implementing, creating individual lookups for each modified type becomes ungainly very quickly.

Now, like most of us, I come from an OOP background, so my first thought following the above discussion was… why not just have a class we inherit the value from and just override the value? This led to the titular exclamation above.

Fine, let’s be a bit sneaky and see what it looks like anywho…

V1 with inheritance V2 with inheritance
open System
type Common(id:string, name:string) =
  member this.id = id
  member this.name = name
type A (id:string, name:string , age:int) =
 inherit Common(id, name)
  member this.id = id
  member this.age = age
type B (id:string, name:string, gender:string) =
  inherit Common(id, name)
  member this.gender = gender
type C (id:string, name:string, height) =
  inherit Common(id, name)
  member this.height = height
let a = new A("123","123",54)
let b = new B("123","123","Female")
let c = new C("123","123",5.9)
printf "%A" a.id
printf "%A" b.id
printf "%A" c.id
open System
type Common(id:string, name:string) =
  member this.id = id
  member this.name = name
type A (id:int, name:string , age:int) =
  inherit Common(string(id), name)
  member this.id = id
  member this.age = age
type B (id:string, name:string, gender:string) =
  inherit Common(id, name)
  member this.gender = gender
type C (id:string, name:string, height) =
  inherit Common(id, name)
  member this.height = height
let a = new A(123,"123",54)
let b = new B("123","123","Female")
let c = new C("123","123",5.9)
printf "%A" a.id
printf "%A" b.id
printf "%A" c.id

It works quite simply! Okay, that’s all well and good. BUT, WE DO NOT WANT INHERITANCE!!!!!!

Okay, so no inheritance, and composition is kind of cumbersome in this instance.

That finally brings us to the problem at hand: Is there a way to do inheritance of attributes without inheriting attributes?

The following solutions were recommended and explored:

  • Establish a sensible data model before trying any form of abstraction.

Ultimately, this is the approach we implemented. However, it does not fully address how to functionally compose common properties.

A valid point (and the one we went with), but it does not address the issue at hand.

  • Object expressions
V1 with object expressions V2 with object expressions
open System
type Common =
  abstract id : String
  abstract name: String
let A id name age =
  let age = age;
  {new Common with
    member this.id = id
    member this.name = name}
let B id name gender=
  let gender = gender
  {new Common with
    member this.id = id
    member this.name = name}
let C id name height=
  let height = height
  {new Common with
    member this.id = id
    member this.name = name}
let a = A "123" "ABC" 54
let b = B "123" "ABC" "Female"
let c = C "123" "ABC" 5.9
let lookupA = a.id
let lookupB = b.id
let lookupC = c.id
open System
type Common =
  abstract id : String
  abstract name: String
let A id name age =
  let age = age;
  let id = id
  {new Common with
    member this.id = string id
 member this.name = name}
let B id name gender=
  let gender = gender
  {new Common with
    member this.id = id
    member this.name = name}
let C id name height=
  let height = height
  {new Common with
    member this.id = id
    member this.name = name}
let a = A 123 "ABC" 54
let b = B "123" "ABC" "Female"
let c = C "123" "ABC" 5.9
let lookupA = a.id
let lookupB = b.id
let lookupC = c.id

This fails our intent as the value of (a). id in the after case returns a string, not an int as intended. The overriding of the value thus doesn’t quite work in this case.

  • Use JsonValue

This approach won’t work for the particular issue at hand. To convert the original type to a JsonObject, modifying and converting it back on every version change defeats the purpose of having a common unchanging type in the first place.

Finally, a few observations:

  • The above information is a prime example of over-engineering too early. The solution we came to (to wait to have a better understanding of how the code will evolve), is a clear indication of why clever solutions are best applied when appropriate rather than everywhere possible.
  • Even functional languages allow for very easy OOP features like inheritance. I was expecting inheritance to be impossible/extremely cumbersome in F#, but it turns out to be one of the easiest to implement.Thus, while the general ethos is anti-OOP features, I doubt they will be removed from F# anytime soon.
  • The more we looked for methods to implement the above, the more we found. For the sake of time constraints, I stayed with the above 3 or 4 approaches, but if there are any that got missed, please let us know.