I'm new to F# and in an attempt to design some types, I noticed how much OOP has affected my design decisions. I had a hard time searching for this particular problem and came up empty-handed.
I will describe what I am trying to do in C# since I am more familiar with the terminology. Let us say that I have an interface specifying some minimal required methods on a container-like class. Let's call it IContainer. Then I have two classes that implement this interface, ContainerA and ContainerB with different underlying implementations that are hidden from users. This is a very common OOP pattern.
I am trying to achieve the same thing in F# only with immutable types to stay in the functional world, i.e. how can I implement a type where its functionality is interchangeable, but the public functions that users will use remain the same:
type 'a MyType = ...
let func1 mytype = ...
let func2 mytype -> int = ...
The definition of MyType is not known and can later be changed, e.g. if a more efficient version of the functions are found (like a better implementation of a container type), but without much effort or requiring a redesign of the entire module. One way is to use pattern matching in the functions and a discriminated union, but that does not seem very scalable.
It is more typical in functional languages to use far simpler types than you would in an OO language.
Modelling shapes is the classic example.
Here is a typical OO approach:
Notice that it's very easy to add new types using this approach, we could introduce a
Triangleor aHexagonclass with relatively little effort. We simply create the type and implement the interface.By contrast, if we wanted to add a new
Perimetermember to ourIShape, we would have to change every implementation which could be a lot of work.Now let's look at how we might model shapes in a functional language:
Now, hopefully you can see that it's much easier to add a
perimeterfunction, we simply pattern match against eachShapecase and the compiler can check whether we've implemented it exhaustively for every case.By contrast, it's now far more difficult to add new
Shapes because we have to go back and change every function which acts uponShapes.The upshot is, whatever form of modelling we choose to use, there are trade-offs. This problem is called The Expression Problem.
You can easily apply the second pattern to your
Containerproblem:Here you have a single type with two or more cases and the unique implementation of functionality for each case is contained in module functions, rather than the type itself.