Functional Programming and Axes of Change
I’ve been working with F# on and off over the past two years, mostly writing parsers for work and play. During that time I’ve come to really enjoy and appreciate the language.
In particular, Discriminated Unions and Pattern Matching are an elegant combination that allow for some pretty succinct expression.
Discriminated Union
Descriminated Unions are a way of representing a complex type as the mathematical “sum” of several other types:
1
2
3
4
type Shape =
| Circle of radius:float
| Square of size:float
| Rectangle of width:float * height:float
In the code sample above, Shape
is a sum type representing the sum or combination of the Circle
, Square
, and Rectangle
types. In F# syntax each component type is called a union case
and is discriminated by it’s label, or case-identifier
.
Once declared, instances of a Discriminated Union can be created (or matched) by using their case-identifer
s:
1
2
3
let circ = Circle (1.0)
let square = Square(5.0)
let rect = Rectangle(1.5, 10.0)
rect
, circ
, and cube
in this sample are all Shape
s
Pattern Matching
Pattern Matching is a common technique for processing Discriminated Unions.
You can define one function that takes the sum type and matches it against patterns to provide custom processing based on the different union cases:
1
2
3
4
5
6
7
8
9
10
11
let getHeight shape =
match shape with
| Circle(radius) -> 2.0 * radius // height is the diameter
| Square(size) -> size // height is length of one side
| Rectangle(width,height) -> height // height is explicit
let getArea shape =
match shape with
| Circle(radius) -> 3.14159 * radius * radius // pi r squared
| Square(size) -> size * size // size squared
| Rectangle(width,height) -> width * height
This allows you to nicely abstract the details of what it means to calculate the height or area from specific shapes.
Compare to C#
In a more object-oriented language like C# you might express this as a base class Shape
with two abstract or virtual methods GetHeight()
and GetArea()
then derive classes that specify those implementations.
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
public abstract class Shape
{
public abstract float GetHeight();
public abstract float GetHeight();
}
public class Circle : Shape
{
private float radius;
public Circle(float radius)
{
this.radius = radius;
}
public override float GetHeight()
{
return 2.0 * radius;
}
public override float GetArea()
{
return 3.14159 * radius * radius;
}
}
I’ve only included C# code for the Circle
shape but it is already more lines of code than the F# example took to describe the processing for all three shapes.
Hopefully it is enough code that you can also see something else.
Impact of a new requirement
Let’s imagine that we have the full F# code above for modeling and calculating the height and area of circles, squares, and rectangles as well as the full C# code of an inheritance structure that expresses the same thing.
Now picture how each code might change based on the following new requirements:
- Add the ability to get the width of a
Shape
. - Add the ability to handle a new type of
Shape
.
The F# idiom of Discriminated Unions and Pattern Matching makes handling the first change as easy as defining a new function called getWidth
and implementing a pattern and formula for each of the three cases; the impact of this change is limited to the new function.
Handling the second change is a bit more complicated.
Adding a new union case to an existing descriminated union is simply a new line expanding the definition of the sum type.
Once that new case is added, however, the change passes like a ripple through the code; every function that expresses a Pattern Match against a Shape
has to be updated to handle the new case.
The impact of this change is spread throughout the codebase.
Axes of Change
The Single Responsibilty Principle advocates that each component of a well-structured software system should have only one reason to change but what happens when the pressure to change might come from different directions?
In the scenario described above it seems that adding a new way to process Shape
instances is change along one axis while adding a new type of shape is change along a perpendicular axis.
Maybe it is just hard to optimize for change along one axis without making it more difficult along another axis.
A more elegant way?
Do you know a functional or idiomatic F# pattern that makes handling this second kind of change more elegant?