Pages

Saturday, November 26, 2011

F# ≥ C# (Discriminated Unions)

It was a boring Black Friday with no interesting discount, so I decide to sit down and continue my blog serials about why F# is better than C#.

I came across my colleague's blog about discriminated union in F#. He proposed an interesting idea to use discriminated unions to do a class hierarchy. I really like this idea but the conclusion in his blog disappoints me a little bit.

Whereas a class hierarchy is "open", in that someone else can come along later and add a new subclass, discriminated unions are "closed", in that the author of the type specifies all the alternatives once-and-for-all (analogy: imagine a "sealed" class hierarchy).  This is often the main factor to consider when trying to decide between a class hierarchy and a discriminated union.

I like the elegant discriminated union. Combining with record, it should be more powerful and concise than C#'s class-based design. If the type hierarchy is closed, that will impose a big problem. Making two different types' data into a single list will be very difficult, this problem increases coding effort. For example, if we have CatType and DogType defined as below:


type CatType =
    | CatA of string
    | CatB of string
    | CatC of string * int
type DogType =
    | DogA of string
    | DogB of string
    | DogC of string * int 

let dogs = [ DogA("a"); DogB("a1") ]
let cats = [ CatA("b"); CatB("b1") ]

if we cannot make a new list which can hold dogs and cats, we have to explicitly access the "dogs" and "cats" to apply certain function. I have to make the discriminated union hierarchy "open" and shows how F# is better than C#.

So my problem becomes to find a way to combine these two list into a new list, something like:

let pets = dogs @ cats

in order to make this happen, I introduce a more general discriminated union type.

type Animal =
    | Cat of CatType
    | Dog of DogType
and convert the dogs and cats list to 



let dogs = [ DogA("a"); DogB("a1") ] |> List.map Animal.Doglet cats = [ CatA("b"); CatB("b1") ] |> List.map Animal.Cat


if you compile the code above, you can get a pets whose type is Animal. Good! Now I can show F# = C#. But how about the greater (>) part? 

In C#, in order to make two incompatible types be stored in list, we have to make them have a common base class or interface. If you can change two types' source code, that won't be a big problem. Adding some interfaces to CatType and DogType only makes your code several lines more. In a big team, you will have to ask around to get permission from code owner(s). 

If CatType and DogType is from a third party library, I believe it will be nightmare. F#'s implementation keeps the original implementation untouched and expands (generalizes) the system in a much cleaner way. Is that more productive?





1 comment:

Anonymous said...

it is ultimate the old aggregation vs. inheritance argument, right?