In my last post on the power of pattern matching, we saw how powerful
match statement is in F#. Using
match allows the compiler to
give us warnings for missing cases, no matter what the type.
Let’s look at how pattern matching changes our design, allowing for an inversion of the usual OO way of polymorphism. Here is an example that is probably familiar to everyone: getting a database connection.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
In our example here, we have two concrete implementers of the
IPaymentRepository, each one with their own implementations. This is
a typical OO way to deal with polymorphism. Usually, “best practices”
would put each of these classes in their own files.
Let’s look at how we would invert the polymorphism of the C# classes and interfaces to use pattern matching.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Notice how we separated our behavior from our types? The
PaymentRepository.InMemory and the
are just empty types, much like an
Enum. We are still able to get
polymorphic behavior from them, using
But why would we want to store our behavior separate from the type?
By storing the behavior separate from the type, changes that effect a
single behavior (adding a new function, changing a function’s api,
removing a function) are easier, because they are all grouped
together. A change to the api of the
GetAll function is harder in
the traditional OO interface structure, requiring modifying several
Similarly, a change requiring adding a new type is difficult in a pattern matching structure, as it will require finding every pattern match and adding in the additional case. Thankfully, the F# compiler checks both pattern matches and interfaces for us, letting us use the best tool for the job!
As to safety, adding a new type is easy with interfaces, but the developer is left without assistance to find all places the concrete classes are instantiated and add the new type. Neither compiler will offer any warnings for a new interface subclass. For pattern matching polymorphism, the compiler will warn that there are missing cases every place a change needs to be made. While harder to add a new type with pattern matching, it is safer.
|Adding a Type||Modifying Behavior|
|OO Interfaces/Classes||Easier / Less Safe||Harder / Safe|
|Pattern Matching Types||Harder / Safe||Easier / Safe|
I almost always find myself modifying the functions of an interface more than I find myself adding new types. For that typical use case, pattern matching is probably the better choice.
Consider the change where we want to add a new function to the
IPaymentRepository interface and change the location of the in
memory dictionary to be stored internally. In the interfaces and
classes example, that requires editing three separate files.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
Here is the change to add a new function in the pattern matching example:
1 2 3 4 5 6 7 8 9 10 11 12
In case you were concerned that these F# types do not have any state,
they actually can have fields just like regular classes. Notice the
Dictionary<int, IPayment> next to the
InMemory type? That is a
field! The new field does not need to be named until used in a pattern
match, so the only time it is named is
payments inside the
GetAll functions after we pattern match
InMemory. In fact, if we
didn’t add it in the pattern match, the compiler would give us a
Between the options of traditional interfaces verses pattern matching, neither way is truly the best for every circumstance: each comes with a trade-off. I liken the trade-offs to the “grain of the data”. Whichever way your system is likely to change the most, that is the way you want to optimize your type. The good news is: in F# you can have a mix of both, and it is relatively easy to convert back and forth depending on how your system is changing the most.
Personally, I find F# pattern matching to be significantly easier to read. The same code in C# requires twice the lines in three separate files, which adds a complexity burden for no reason. The F# code is safer, smaller, and easier to modify than the C# equivalent.
If you write code in C# or VB.NET right now, you could add in a project in F# today. All three languages are callable from the other two, so you could start by breaking out a small library that uses these feature immediately. F# modules and classes are callable from C# just like any other DLL library. In my mind, this is what sets F# apart from other languages: it is more powerful and safe than C#, but with high performance and interoperability with existing C# libraries.
If you want additional reading on the topic of polymorphism, check out section 2.4 in SICP.
Discussion in the HN comments