OneOf<Us> Discriminated Unions in C#
Tags: dotnet, csharp, functional
Published: 2019-09-22 23:43:30

Ever wish you could act on different types of variables - effectively switching by object type and taking different action depending on which class is present? Discriminated Unions from functional programming languages offer an answer to this. In this article I explore the good and bad of Discriminated Unions in C# and offer some thoughts on whether this might be right for your project.

[Discriminated Unions](https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/discriminated-unions) are a functional programming convenience that indicates that something is *one of* several different types of objects. For example, a User might be an unauthenticated user, a regular user, or an administrator.

While [discriminated unions are being evaluated for addition to the C# language](https://github.com/dotnet/csharplang/issues/113), they are not presently available, however, the [OneOf](https://github.com/mcintyre321/OneOf) library provides an alternative for those wanting to use discriminated unions before we have official language support for it.

Let me show you how it works and why you may or may not want to use it.

# How OneOf Works

### Returning a OneOf Result

This example is from a hobbyist game development project I'm tinkering with. I needed a routine that has a technician attempt to work on a work item. Depending on a random number result, they will either get a `WorkCompletedResult`, a `ProgressMadeResult`, or a `SetbackResult`.

*Note: I ask that you suspend your disbelief as to whether or not these classes should even exist and look at this as purely a technical demonstration of OneOf*

```cs
private OneOf<WorkCompletedResult, ProgressMadeResult, SetbackResult> 
        GetWorkOnItemResult(CrewContext context, WorkItem item)
{
    var roll = context.GetRandomNumber(1, 20);

    // Something went wrong. Likely a mistaken assumption or bad requirement
    if (roll == 1)
    {
        return new SetbackResult(Priority.High);
    }

    // We got a massive success or we've previously made significant progress
    if (roll >= 20 - item.ProgressMade)
    {
        return new WorkCompletedResult(item);
    }

    // Okay, not a great roll, let's call it a setback
    if (roll < 3)
    {
        return new SetbackResult(Priority.Normal);
    }

    // Just a bit of progress
    return new ProgressMadeResult(item);
}
```

Let's unpack this. The only really unusual part about this code is the method's return type: `OneOf<WorkCompletedResult, ProgressMadeResult, SetbackResult>`.

This is, frankly, a fairly ugly way of using generics to say that the result of the method will be one of the different types of generic arguments.

From there, as long as the method returns one of these results no code changes are needed.

### Working with OneOf Results

So, now that we have a method returning a `OneOf` result, what can we do with this?

The following example is from a node in a behavior tree I'm building (stay tuned for a future article on more details on behavior trees in general). This code switches off of the result of the call too the `GetWorkOnItemResult` method defined above and handles each case differently.

```cs
private BehaviorResult<CrewContext> ProcessCrewWorkOnItem(CrewContext context, WorkItem ticket)
{
    var result = GetWorkOnItemResult(context, ticket);

    result.Switch(
        completed =>
        {
            ticket.Status = WorkItemStatus.ReadyForReview;
            AddMessage(context, $"{context.CrewMember.FullName} finishes work on {ticket.Title}");
        },
        progress =>
        {
            ticket.ProgressMade++;
            AddMessage(context, $"{context.CrewMember.FullName} made steady progress on {ticket.Title}");
        },
        setback =>
        {
            if (setback.Severity > Priority.Normal)
            {
                ticket.ProgressMade -= 3;
                AddMessage(context, $"{context.CrewMember.FullName} encountered a major setback on {ticket.Title}");
            }
            else
            {
                ticket.ProgressMade -= 1;
                AddMessage(context, $"{context.CrewMember.FullName} encountered a setback on {ticket.Title}");
            }
        }
    );

    return MatchedResult(context);
}
```

The `.Switch` function takes an `Action` parameter for each generic type defined, giving you access to a strongly-typed instance of that object type as we see with the use of the `setback` parameter, for example. 

These arguments are matched in order to the order that the generic types are defined on the `OneOf` type declaration. In our example, the first parameter will be an `Action<WorkCompletedResult>`, the second will be an `Action<ProgressMadeResult>`, and the third will be `Action<SetbackResult>`. This is why we can use `setback.ProgressMade`, a property declared on the `SetbackResult` instance.

So, `OneOf` lets us return one of several different options and then gives you a method to do something different depending on which one of those types you encounter.

If you needed to structure your code so that each case returns a value, you can use `Match` instead of `Switch` as follows:

```cs
private BehaviorResult<CrewContext> ProcessCrewWorkOnItem(CrewContext context, WorkItem ticket)
{
    var result = GetWorkOnItemResult(context, ticket);

    var message = result.Match(
        completed =>
        {
            ticket.Status = WorkItemStatus.ReadyForReview;
            return $"{context.CrewMember.FullName} finishes work on {ticket.Title}";
        },
        progress =>
        {
            ticket.ProgressMade++;
            return $"{context.CrewMember.FullName} made steady progress on {ticket.Title}";
        },
        setback =>
        {
            if (setback.Severity > Priority.Normal)
            {
                ticket.ProgressMade -= 3;
                return $"{context.CrewMember.FullName} encountered a major setback on {ticket.Title}";
            }
            else
            {
                ticket.ProgressMade -= 1;
                return $"{context.CrewMember.FullName} encountered a setback on {ticket.Title}";
            }
        });

    AddMessage(context, message);

    return MatchedResult(context);
}
```

As you can see, this is nearly identical to `Switch` except `Match` uses a `Func<T, TResult>` instead of an `Action<T>`.

# Weak Areas

So, this is an interesting trick, but where does it fall down?

### Type Aliases

Unlike languages like TypeScript and F#, you can't define a simple reusable alias for a Discriminated Union, meaning that if you pass around the same combinations of types, you have to use the same `OneOf` syntax on return types and parameter values and you can't use a simple type alias.

For reference: a type alias for a Discriminated Union in TypeScript looks like this:

```ts
type User = GuestUser | StandardUser | Administrator;
```

### Code Completion

The code completion is very limited on the `Switch` and `Map` functions. The most irritating aspect is having to remember the ordering of types in the `OneOf` definition.

### Order Swapping

If you declare two different `OneOf` return types with the parameters in different orders, they will not be swappable between each other

### Acting only on a Specific Type

Let's say you want to look at a result and only do something if the return is a `StetbackResult`. You could either use `Switch` with empty parameters, or you can use the `AsT1` and `IfT1` members. Sadly, these are their actual names, so it becomes easy to mix up which type is in which order yet again.

---

# My Opinion

So, this is something you *can* do in C#. The more important question is: Should you?

My answer to that, after investigating the library is: probably not. It's a cool trick, but the language constraints hamstring the viability of any library.

For now, unless you have some very targeted usages, my recommendation is that you avoid the added complexity of `OneOf` and either wait for official language support or add a small F# library and reference it from C# or VB .NET code.

---

If you have other thoughts or know of another way of getting Discriminated Unions work better in C#, please let me know.

---

*Photo by Markus Spiske on Unsplash*