Suppose I had two classes:
public class Triangle {
public float Base { get; set; }
public float Height { get; set; }
public float CalcArea() { return Base * Height / 2.0; }
}
public class Cylinder {
public float Radius { get; set; }
public float Height { get; set; }
public float CalcVolume() { return Radius * Radius * Math.PI * Height }
}
We have here the descriptions of two geometric shapes along with an operation in both.
And here's my attempt in F#:
type Triangle = { Base: float; Height: float }
module TriangleStuff =
let CalcArea t =
t.Base * t.Height / 2.0
type Cylinder = { Radius: float; Height: float }
module CylinderStuff =
let CalcVolume c =
c.Radius * c.Radius * Math.PI * c.Height
Suppose I made an observation about these two classes (they both have Height
!) and I wanted to extract an operation that made sense for anything that had a height property. So in C# I might pull out a base class and define the operation there, as follows:
public abstract class ShapeWithHeight {
public float Height { get; set; }
public virtual bool CanSuperManJumpOver() {
return Height == TALL; // Superman can *only* jump over tall buildings
}
public const float TALL = float.MaxValue;
}
public class Triangle : ShapeWithHeight {
public float Base { get; set; }
public float CalcArea() { return Base * Height / 2.0; }
public override bool CanSuperManJumpOver() {
throw new InvalidOperationException("Superman can only jump over 3-d objects");
}
}
public class Cylinder : ShapeWithHeight {
public float Radius { get; set; }
public float CalcVolume() { return Radius * Radius * Math.PI * Height }
}
Note how individual subclasses might have their own ideas as to the implementation of this operation.
Closer to the point, I might have a function somewhere which can accept either a Triangle or a Cylinder:
public class Superman {
public void JumpOver(ShapeWithHeight shape) {
try {
if (shape.CanSuperManJumpOver()) { Jump (shape); }
} catch {
// ...
}
}
}
.. and this function can accept either a Triangle or a Cylinder.
I'm having trouble applying the same line of thinking to F#.
I've been doing some reading about functional languages. The conventional thinking is that it is preferred to express algebraic value types rather than inherited classes. The thinking goes that it's better to compose or build richer types out of smaller building blocks rather than starting with an abstract class and narrowing from there.
In F# I want to be able to define a function which takes an argument that is known to have a Height property, and work with it somehow (i.e. the base version of CanSuperManJumpOver
). How should I structure these types in a functional world to achieve it? Does my question even make sense in a functional world? Any comments on the thinking is most welcome.
In my opinion, the C# design that you are describing is fundamentally wrong - you defined a type ShapeWithHeight
with a virtual method CanSuperManJumpOver
but the method cannot be implemented for one of the concrete instances (2D triangle) and you have to throw an exception instead.
One of the key principles when modelling a domain in F# is that invalid states should not be representable (see this great article for more). Your design breaks this - because you can construct a triangle and invoke an operation on it, but the operation is invalid.
So, the first thing to consider would be, what is actually the domain that you are trying to model? (This is a bit hard to guess from your examples, but let me try...) Let's say that you have some objects and superman can jump over 3D shapes, but not over 2D shapes. You could use a discriminated union to distinguish between these two kinds of shapes:
type Height = float
type Shape2DInfo =
| Triangle of float * float
type Shape3DInfo =
| Cylinder of float
type Shape =
| Shape2D of Shape2DInfo
| Shape3D of Height * Shape3DInfo
The trick is that for all 3D shapes, we now have height directly available in the Shape3D
case - so you can always get height of a 3D shape (regardless of which specific shape it is - here, only cylinder). For 2D shapes, I did not include height, because they might, or might not have it...
Then you can write a jumping function that pattern matches on a shape and handles the three different cases - the shape is not jump-able, the shape is too small or the shape is high enough:
let jumpOver shape =
match shape with
| Shape2D _ -> printfn "Cannot jump!"
| Shape3D(height, _) ->
if height = Double.MaxValue then printf "Jumped!"
else printfn "Too boring!"
In summary - if you have an abstract class with some properties in C#, the closest thing in F# (if you want to use functional design, rather than OO design) is to use a type that stores the common properties (Shape
) and contains value of another type (Shape3DInfo
) that specifies the details specific for each sub class.
In a functional programming paradigm you would start with the functions, and work out the types from that. So the starting point would be the function
let CanSupermanJumpOver height =
height = TALL
You would only start thinking about polymorphism between triangles and cylinders when you have a question which needs to apply the CanSupermanJumpOver function on both interchangeably. Basically the OO paradigm focusses on hiding the internal data structures in use. The functional paradigm focusses on encapsulating complex process logic, but the data structures are transparent. The trick is trying to combine both approaches without compromising the benefits of either. That is something I've been struggling with over the last while.
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