Emulating Discriminated Unions in C# using Records
Sep 18, 2021
Discriminated Union / Sum Type is one of the common features found amoung strong typed functional languages like F#, Haskell etc. Ever since I started using them in F#, I can’t stop wondering why such an useful feature is not available on the popular languages. C# has been adding a lot of functional features over the last few years. So, we are going to achieve something close to that in C# using Records and Inheritance.
In F#, we can create a Discriminated Union for Shape as shown below.
type Shape = Circle of int
| Square of int
| Rectangle of int * int
| Triangle of int * int * int
| Line of int
Discriminated Unions alone cannot be beneficial without powerful Pattern Matching constructs built into the language. In the case of F#, calculating area for any shape is easy as writing a function with Pattern Matching.
let getArea s =
match s with
| Circle(r) -> Math.PI * float(r) * float(r)
| Square(x) -> float(x*x)
| Rectangle(l, b) -> float(l*b)
| Triangle(a, b, c)
when a = b && b = c
-> Math.Pow(float(a), 2.0) * Math.Sqrt(3.0)/4.0
| Triangle(a, b, c)
when Math.Pow(float(c), 2.0) =
Math.Pow(float(a), 2.0) + Math.Pow(float(b), 2.0)
-> float(a * b) / 2.0
| _ -> 0.0
Now lets see how we can express this in terms of Records in C#.
Records are introduced in C# 9.0 which are special classes with value equality
and simpler syntax. It takes a constructor with parameters and all the parameters mentioned in the constructor are automatically promoted into Properties with {init; get;}
. Records are immutable and compared by values by default, just like any F# type. (However, there are some differences like deep equality is not guaranteed when reference types are used within Records which we are not going to give attention now.)
public record Shape{};
public record Circle(int Radius) : Shape;
public record Square(int Side) : Shape;
public record Rectangle(int Width, int Height) : Shape;
public record Triangle(int A, int B, int C) : Shape;
public record Line(int Length): Shape;
So here we create an empty Shape
record which is being inherited by all the other shapes but with different set of parameters. Inheritance is the only relationship among these types which enables us to pass different shape objects like Circle
or Square
in the place of Shape
. As I said earlier, Discriminated Unions are not useful on its own without Pattern Matching and C# has been adding good Pattern Matching support over last few versions. This allows us to compare type of the Record and evaluate an expression based on that.
So the getArea
function would like the one below in C# 9.0.
static double Area(Shape s) =>
s switch {
Circle c => Math.PI * c.Radius * c.Radius,
Square square => square.Side * square.Side,
Rectangle r => r.Width * r.Height,
Triangle t // Equilateral Triangle
when Math.Pow(t.C,2) ==
Math.Pow(t.A, 2) + Math.Pow(t.B, 2)
=> t.A * t.B / 2,
Triangle t // Right angled Triangle
when t.A == t.B && t.B == t.C
=> Math.Pow(t.A, 2) * (Math.Sqrt(3)/4),
Line or _ => 0,
};
It is pretty close to the F# version of the code. Honestly I didn’t expect C# would catch up with such good Pattern Matching so soon. But it’s not quite common yet to see C# code similar to this as it will take time for the developers to adopt to the new way of thinking to solving problems.