I'm starting to learn F# with the wonderful site https://fsharpforfunandprofit.com
While reading about pattern matching in the entry about four key concepts that differentiate F# from a standard imperative language, I found this sentence (emphasis mine):
These kinds of choice types can be simulated somewhat in C# by using subclasses or interfaces, but there is no built in support in the C# type system for this kind of exhaustive matching with error checking.
That seems strange to me, because I think we can obtain an exactly equivalent result with a method definition in an interface (or with an abstract method in an abstract class) in C#: it enforces all the inheriting classes to implement that method, in the same way that in the F# code it enforces the draw method to provide an implementation for all the "inheriting" types.
The difference is that in the functional case all the implementations are in the same method, while in the object oriented case, each implementation is encapsulated in its class... but conceptually you get the same enforcement in both cases, so I don't see any benefit in the functional approach.
Am I missing something? Can somebody clarify this for me?
The key insight is that there are two ways of modeling domain (broadly speaking).
Let's take classes and interfaces. Say you declare IShape and make Circle, Rectangle, and all others implement it. Great. What methods are you going to have on IShape? Let's say Draw. So far so good.
Now imagine you implement yourself a dozen shapes. And then, a few months later, you find yourself in need of another operation. Let's call it IsEmpty. What do you do? You add IsEmpty to IShape, and then you go into each of the dozen classes and add IsEmpty to them. A bit of a hassle, but ok, you can do it.
A few months down the road you want to add another operation. Then another. And another. You get tired of this pretty quickly, but that's still fine, you grit your teeth, but you do it.
But then there's the next problem: somebody else who is using your library wants to add an operation of their own. What do they do? They can't modify the IShape interface, it's in your library. They can ask you to do it and republish the library (not very efficient, is it?). Or they can implement the operation in terms of if + is - i.e. if (shape is Circle) { ... } else if (shape is Rectangle) { ... }, and so on. But then they hit the very difficulty described in the article you linked - the compiler won't protect them from missing a shape!
On the other hand, take discriminated unions. You describe the union, and then add operations all you want, left and right. Every operation handles all cases within itself (and the compiler verifies that all cases are indeed handled), and you can even add new operations in other projects that reference your library without modifying the original code. Total nirvana!
But then, a few months down the road, you find out that you need another case - say Triangle. You can, of course, add this case to the type, but then you'd have to go and add handling for it in every single operation. Even worse: those people who are using your library - their code will break when they get the latest version, and they will have to modify their additional operations as well. Tedious!
So it seems that there are two different mutually exclusive ways:
This is a well known problem in language design. So well known it has its own name - the "Expression Problem". There are actually languages that let you have your cake and eat it, too - Haskell has type classes, Clojure has protocols, Rust has traits, etc. Neither of the solutions that I've seen are elegant enough in practice, to the point of getting one to wonder whether solving the expression problem is even worth it.
F# does not solve this problem[1] - you can't have both ways at the same time. However, F# at least supports both ways separately: classes+interfaces for "open world", discriminated unions for "closed world". C#, on the other hand, supports only "open world".
More importantly, it turns out that in real programs "closed world" modeling is far more useful than "open world". Programs modeled this way turn out to be far more understandable, less buggy, more concise. "Open world" models usually come in useful when you expect your program to be extended after it's written, by somebody you don't necessarily know - aka "plugins". This situation does happen, but not that often.
[1] if you don't count shenannigans with statically resolved type parameters, which don't reliably work in all circumstances anyway
So, conceptually, we are talking about two quite different approaches for modelling a domain.
Consider the functional approach that we see described in the article:
type Shape = // define a "union" of alternative structures
| Circle of radius:int
| Rectangle of height:int * width:int
| Point of x:int * y:int
| Polygon of pointList:(int * int) list
let draw shape = // define a function "draw" with a shape param
match shape with
| Circle radius ->
printfn "The circle has a radius of %d" radius
| Rectangle (height,width) ->
printfn "The rectangle is %d high by %d wide" height width
| Polygon points ->
printfn "The polygon is made of these points %A" points
| _ -> printfn "I don't recognize this shape"
The key point here is that Shape defines that there are four possible options: Circle, Rectangle, Polygon and Point.
I cannot invent a new union case somewhere else in my program, Shape is strictly defined to be one of these options and, when pattern matching, the compiler can check I haven't missed one.
If I use a C# style model:
interface IShape {}
class Circle : IShape {}
class Rectangle : IShape {}
class Point : IShape {}
class Polygon : IShape {}
The possible types are unbounded. In one or more other files, I can simply define some more if I feel like it:
class Triangle : IShape {}
class Pentagon : IShape {}
class Hexagon : IShape {}
You can never know how many IShapes there might be in existence.
This is not true of the F# Shape we defined above. It has four options and only four.
The discriminated union model is actually very very powerful because often, when we're modelling a domain in software, the possible states in that domain are actually a relatively small and succinct set of options.
Let's take another example from the F# for Fun and Profit site of a Shopping cart:
type ShoppingCart =
| EmptyCart
| ActiveCart of unpaidItems : string list
| PaidCart of paidItems : string list * payment: float
If I model my cart in this way, I'm vastly reducing the scope for possible invalid states because my cart can be in one of these three states and no others.
Interfaces and classes will let you model exactly the same states but they will not prevent you from also creating an arbitrary number of additional states that are completely meaningless and irrelevant to your domain.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With