State of the Discriminated Union
Third party goodness for C#, as the state of the (native) discriminated union is still unclear.
You might have heard of the discriminated union type, as it’s called in F#. It’s a special type that’s available in a range of statically-typed multi-paradigm languages, including Rust, Swift, Scala and Typescript. But C# developers are still missing out.
Design discussions are still ongoing in dotnet/csharplang/issue#113, and it seems unlikely that it will make it into C# 11.
So what is it and why do people want it?
The discriminated union type simply states that data can be one of a range of pre-defined types. Like this:
type SaveCustomerResult =
| CustomerCreated
| CustomerUpdated
| SaveFailed
SaveCustomerResult
is compile-time guaranteed be of either type CustomerCreated
, CustomerUpdated
or SaveFailed
. It can also be inferred from any of the union’s child types. Which allows you do write (pseudo) code like this:
var status = save(customerData)
match
| CustomerCreated => "customer was created"
| CustomerUpdated => "customer was updated"
| SaveFailed => "could not save customer"SaveCustomerResult save(customer) {
try {
if (exists(customer.id)) {
update(customer)
return CustomerUpdated
} create(customer)
return CustomerCreated
}
catch {
return SaveFailed
}
}
If you’re doing something that has a range of outcomes, discriminated unions help you describe it simply and with type safety, leaving no stone unturned — discouraging logic based on null
cases or polymorphism.
Harry McIntyre to the Rescue
Harry and a range of open-source contributors provide the OneOf library for dotnet. Which brings F# style d̶i̶s̶c̶r̶i̶m̶i̶n̶a̶t̶e̶d̶ unions for C#. The library has been available since 2016 and is ever-increasing in popularity. Harry is also an active voice in the design discussions for a potential C# native implementation.
On the d̶i̶s̶c̶r̶i̶m̶i̶n̶a̶t̶e̶d̶ part
For a union to be discriminated it must consist only of different types. But due to limitations of C#’s generic type constraints, OneOf<string, string>
will compile. This type check limitation might be related to why we don’t have native discriminated unions in C# yet.
Let’s look at some OneOf code
First off, OneOf brings exhaustive pattern matching with Match
and Switch
. Meaning that you must explicitly declare all outcomes or else it won’t compile. Compare to the built-in switch
which let’s you compile and forget that you have unmanaged cases.
OneOf<Yes, No, Maybe> union ... ;// Use Switch when you don't want to return anything
union.Switch(
yes => Console.WriteLine("yes"),
no => Console.WriteLine("no"),
maybe => Console.WriteLine("maybe"));// Use Match to return a value
string response = union.Match(
yes => "yes",
no => "no",
maybe => "maybe");
Using the pseudo code example above we can write it with C# and OneOf instead. For more and better examples checkout the GitHub repo of OneOf.
var status = Save(customerData)
.Match(
created => "customer was created",
updated => "customer was updated",
failed => "could not save customer");OneOf<CustomerCreated,
CustomerUpdated,
SaveFailed> Save(CustomerData customer)
{
try
{
if (_repository.Exists(customer.id)) {
_repository.Update(customer);
return new CustomerUpdated(customer);
} _repository.Create(customer);
return new CustomerCreated(customer);
}
catch
{
return new SaveFailed(customer);
}
}
Other Types Derived from Discriminated Unions
While this post focused on the union type itself, there is a whole functional world of possibilities opening up just by having the option to say that something is one-of some other things. I’m thinking about certain monads that help remove if
and else
branching logic in your code, making it more readable and less error prone.
I will write a follow-up post on this, where I talk about my OneOf.Monads library.
Until next time!