Discriminated unions in C#

One of the best features about true functional languages is the discriminated union (DU). These allow you to create a type that can represent one of a number of different types, but without requiring some complex inheritance hierarchy.

Sadly, C# doesn’t have discriminated unions yet (I’m still hopeful). However, I came across an excellent suggestion on StackOverflow, which showed a reasonably simple way of implementing a DU with three type parameters in C#. However, it had a lot of boilerplate code, which was removed in a pastebin modification posted in the comments.

However, even that was a little restricting, in that the Union3 class shown was fixed to three generic type parameters. If you wanted a DO of two, four or any other number of types, you’d need to modify the class, or generate a new one named Union4 or whatever. This felt clunky.

The other issue with this code is that it only implements a Match method, which is fine for when you want to return a value based on the type, but many time we only want to do something, not return something. That was easily fixed by adding a Switch method, that looked suspiciously similar to Match.

Taking inspiration from the way you can create a Tuple with a variety of numbers of type arguments, I wrote a script that you can run in LinqPad that will generate Du classes to do the same. I restricted it to 5 types, as if you had more than that, I suspect your design may be suspect, but there is no reason you can’t change the upper limit in the first line to anything you like.

foreach (int n in Enumerable.Range(2, 5)) {
  string types = string.Join(", ", Enumerable.Range(1, n).Select(e => $"T{e}"));
  Console.WriteLine("public class Du<" + types + "> {");
  for (int i = 1; i <= n; i++) {
    Console.WriteLine($"  private readonly T{i} _item{i};");
  }
  Console.WriteLine("");
  for (int i = 1; i <= n; i++) {
    Console.WriteLine($"  public Du(T{i} item) {{ _item{i} = item; tag = {i}; }}");
  }
  Console.WriteLine("  private readonly int tag;");
  Console.WriteLine("");

  string funcArgs = string.Join(", ", Enumerable.Range(1, n).Select(i => $"Func<T{i}, T> f{i}"));
  Console.WriteLine($"  public T Match<T>({funcArgs}) {{");
  Console.WriteLine("    switch (tag) {");
  for (int i = 1; i <= n; i++) {
    Console.WriteLine($"      case {i}: return f{i}(_item{i});");
  }
  Console.WriteLine("      default: throw new Exception(\"Unrecognized tag value: \" + tag);");
  Console.WriteLine("    }");
  Console.WriteLine("  }");

  string actionArgs = string.Join(", ", Enumerable.Range(1, n).Select(i => $"Action<T{i}> a{i}"));
  Console.WriteLine($"  public void Switch({actionArgs}) {{");
  Console.WriteLine("    switch (tag) {");
  for (int i = 1; i <= n; i++) {
    Console.WriteLine($"      case {i}:");
    Console.WriteLine($"        a{i}(_item{i});");
    Console.WriteLine($"        break;");
  }
  Console.WriteLine("      default: throw new Exception(\"Unrecognized tag value: \" + tag);");
  Console.WriteLine("    }");
  Console.WriteLine("  }");

  Console.WriteLine("}");
  Console.WriteLine("");
}

I thought about doing this as a .tt template, but given that you’d only need to generate it the once, it didn’t seem worth it.

Running this code gave Du classes that looked like this (two parameter version shown)…

public class Du<T1, T2> {
  private readonly T1 _item1;
  private readonly T2 _item2;

  public Du(T1 item) { _item1 = item; tag = 1; }
  public Du(T2 item) { _item2 = item; tag = 2; }
  private readonly int tag;

  public T Match<T>(Func<T1, T> f1, Func<T2, T> f2) {
    switch (tag) {
      case 1: return f1(_item1);
      case 2: return f2(_item2);
      default: throw new Exception("Unrecognized tag value: " + tag);
    }
  }
  public T Switch(Action<T1> a1, Action<T2> a2) {
    switch (tag) {
      case 1:
        a1(_item1);
        break;
      case 2:
        a2(_item2);
        break;
      default: throw new Exception("Unrecognized tag value: " + tag);
    }
  }
}

Set your upper limit, run the script, and copy the full output into a .cs file in your project, and you can use it as shown in the SO answer with any number of type parameters.

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.