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?