How F# can help with the pitfalls of C# enumerations

I don’t get it. Even in a statically typed language like C# you can change the domain model and your application will still compile as if nothing happened. In this post we will address this and see how F# can help with such pitfalls of C# enumerations.

What’s the problem?

The problem with enumerations is simply that when we add new cases especially in a large code base, it can be really hard to find and update all dependent source code which might lead to errors at runtime.

The compiler will not help to find all the places that have to be updated. A text-based search isn’t reliable.

Here is how things can go wrong.

The application might silently fail …

Let’s say we have an enum Directions:

public enum Direction { Left, Right }

Now we add a case for Up.

public enum Direction { Left, Right, Up }

In switch statements the default case might be selected accidentally:

public static string ToDisplayName(this Direction dir)
{
    switch (dir)
    {
        case Direction.Left: return "Go to the left.";
        case Direction.Right: return "Go to the right.";
        default: return "unknown direction";
    }
}

… or it might explode at runtime …

If there is no default case, an exception will be thrown:

public static string ToDisplayName(Direction dir)
{
    switch (dir)
    {
        case Direction.Left: return "Go to the left.";
        case Direction.Right: return "Go to the right.";
    }

    throw new Exception("unknown direction");
}

… and finally enumerations can represent invalid state

This code will compile:

var dir = (Direction) 42;

And it certainly should not because this leads to the other problems described before.

As we can assign arbitrary integer values to an enum also unit testing cannot be a sufficient strategy to find unhandled cases.

What does MSDN suggest?

MSDN addresses these problems and for more robust programming they suggest the following:

If other developers use your code, you should provide guidelines about how their code should react if new elements are added to any enum types. —enum (C# Reference)

However, coding conventions are not checked at compile time.

I think we can do better.

How we can do better

I recently had that. I added a new enumeration case and searched and updated all usages. All tests passed. Anyway, I wasn’t 100% confident that I hadn’t missed a thing.

Wouldn’t it be nice if the compiler just pointed out all the places we have to update after we change the model?

Replacing enums with F#’s discriminated unions (with empty cases)

…and improve the reliability and robustness of our C# code.

It is possible to have the compiler check that all places are updated. And then we can have really high confidence that everything will work as expected. We will simply know.

We can achieve this with the help of F#’s discriminated unions.

Even though discriminated unions can do a lot more they can be used in a way that is very similar to enumerations.

Let’s look at what we have to do.

First we add an F# class library project to the solution and reference it from the other projects.

Then we define the type Directions as a discriminated union with the two empty cases Left and Right:

type Directions =
    | Left
    | Right

After we have built the F# project we can use it from the C# code.

Instantiating a discriminated union

Instances of Directions can be created with static properties like this:

var left = Directions.Left;

var right = Directions.Right;

Evaluating equality

Discriminated unions provide properties to check for their case.

var left = Directions.Left;
Assert.That(left.IsLeft);
Assert.That(left.IsRight, Is.False);

For discriminated unions with empty cases checking for the case and evaluating equality is actually the same:

var right = Directions.Right;
Assert.That(right.Equals(Directions.Right));
Assert.That(right == Directions.Right);

Pattern matching instead of switch statements

In F# there are no switch statements. Instead match expressions are used for pattern matching.

The ToDisplayName function can be implemented like this in F#:

let toDisplayName dir =
    match dir with
    | Left  -> "Go to the left."
    | Right -> "Go to the right."

If we add the value Up now, we will get a compiler warning.

type Directions =
    | Left
    | Right
    | Up

alt text

The compiler will warn us if a pattern match is not exhaustive. This way we cannot miss to update code that otherwise might fail at runtime.

Simulating pattern matching in C#

So this is nice. But how can we take advantage of this exhaustive case check in our C# project?

We will simply define an extension method Match that we can call from our C# code, as described in this article by Mauricio Scheffer.

For each case we have to pass a generic function that creates the corresponding result.

Here is the implementation:

open System.Runtime.CompilerServices
open System

[<Extension>]
type DirectionsExtensions =
    [<Extension>]
    static member Match(dir, (onLeft:Func<_>),
                             (onRight:Func<_>),
                             (onUp:Func<_>)) =
        match dir with
        | Left   -> onLeft.Invoke()
        | Right  -> onRight.Invoke()
        | Up     -> onUp.Invoke()

Now we can replace the switch statement by calling the Match function like this:

public static string ToDisplayName(this Directions dir)
{
    return dir.Match(
        onLeft: ()  => "Go to the left.",
        onRight: () => "Go to the right.",
        onUp: ()    => "Go up.");
}

Like this there is no way that we can miss to handle a case.

There are no disadvantages compared to switch statement. Sure, switch statements can be non-exhaustive. But I hope that it has become clear by now that this is not good.

Simulating other enum behaviors and members

If you rely on any of the built-in behavior or members of the C# enumeration type, they can easily be simulated.

Explicit conversion to the underlying numeric type can be handy when storing enum values in a database or when comparing enum values to each other.

We can implement static and instance members for discriminated unions that simulate this behavior like this:

type Directions =
    | Left
    | Right
    | Up
with 
    member x.ToInt =
        match x with
        | Left  -> 1
        | Right -> 2
        | Up    -> 3
    static member op_GreaterThan (a : Directions, b : Directions) =
        a.ToInt > b.ToInt
    static member op_LessThan (a : Directions, b : Directions) =
        a.ToInt < b.ToInt
    static member Parse fromInt =
        match fromInt with
        | 1 -> Some Left
        | 2 -> Some Right
        | 3 -> Some Up
        | _ -> None

Likewise we can implement other behavior according to our needs.

Note that the “deserialization” function Parse returns an optional value. An alternative would be to define a case for Directions.None.

Or e.g. instead of Enum.GetNames we can use FSharpType.GetUnionCases:

FSharpType.GetUnionCases(typeof(Directions), FSharpOption<BindingFlags>.None)
    .Select(x => x.Name);

When not to use this

When the scope of an enumeration type is really small and when it is not likely that it will increase in the future, it might be unnecessary overhead to use a discriminated union instead.

Also when we use an enumeration type to define bit flags we should stick with it.

When to use this

In any other case, I would say, it makes sense because it makes programming more reliable and robust.

Designing with types

One last thought. If we use enums or discriminated unions with only empty cases, this might be a sign that our domain model is not ideal.

Here is an example of a poorly designed model to represent a shape:

public enum ShapeType { Square, Rectangle }

public class Shape
{
    public float Width { get; set; }
    public float Height { get; set; }
    public ShapeType Type { get; set; }
}

With this model it is easy to represent invalid state e.g. like this:

var shape = new Shape
{
    Width = 1.5f,
    Height = 2.5f,
    Type = ShapeType.Square,
};

With discriminated unions it is possible to design the model in a way so that invalid state is unrepresentable:

[<Measure>] type cm

type Shape =
    | Square of sidelength:float<cm>
    | Rectangle of width:float<cm> * height:float<cm>

We can instantiate shapes in C# like this:

var square = Shape.NewSquare(_sidelength: 1.5);

var rectangle = Shape.NewRectangle(
    _width: 1.5,
    _height: 2.5);

So extensive use of enums or empty discriminated unions might be a code smell. Consider to model your domain with algebraic data types instead.

Conclusion

Switch statements are not exhaustively checked. Therefore we have no guaranty that all cases of an enumeration type are handled even if our program compiles.

Furthermore enums can carry invalid values.

To make programming more reliable and robust we can replace enums with F#’s discriminated unions and use them from C# projects quite easily.

To do this we have to define a discriminated union with empty cases and a Match extensions method.

Then we can replace switch statements by calling the Match function.

If we add more cases to the type, the compiler will produce warnings or errors. This way it will be easy to find and update all usages and we can have very high confidence that our program won’t fail at runtime.

Finally, extensive use of enums or empty discriminated unions might be a code smell. Consider to model your domain with algebraic data types instead.

Please check out these posts that are related to designing with algebraic data types:

Resources