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:
type IShape =
abstract member Area : double
type Circle(r : float) =
member this.Area = System.Math.PI * r ** 2.0
interface IShape with
member this.Area = this.Area
type Rectangle(w : float, h : float) =
member this.Area = w * h
interface IShape with
member this.Area = this.Area
Notice that it's very easy to add new types using this approach, we could introduce a Triangle
or a Hexagon
class with relatively little effort. We simply create the type and implement the interface.
By contrast, if we wanted to add a new Perimeter
member to our IShape
, 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:
type Shape =
|Circle of float
|Rectangle of float * float
[<CompilationRepresentation (CompilationRepresentationFlags.ModuleSuffix)>]
module Shape =
let area = function
|Circle r -> System.Math.PI * r ** 2.0
|Rectangle (w, h) -> w*h
Now, hopefully you can see that it's much easier to add a perimeter
function, we simply pattern match against each Shape
case 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 Shape
s because we have to go back and change every function which acts upon Shape
s.
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 Container
problem:
type Container =
|ContainerA
|ContainerB
let containerFunction1 = function
|ContainerA -> ....
|ContainerB -> ....
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.