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 Unions

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-identifers:

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 Shapes

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:

  1. Add the ability to get the width of a Shape.
  2. 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?