F# Discriminated Unions vs. C# Class Hierarchies: A Design Perspective
Tags: csharp, fsharp, architecture, ddd
Published: 2023-09-25 20:21:46

Let's expand on the previous `User` class example. Let our user be a customer of an online store.

Once a customer finalizes an order, any post-dispatch modifications risk integrity breaches. It's essential to ensure the order data's privacy and integrity. 

```csharp
public class User
{
    // ...
    
    public IEnumerable<Order> Orders { get; init; }
}

public partial class Order
{
    public bool IsValidated { get; init; }
    public decimal? Price { get; init; }
    public IEnumerable<OrderItem> OrderItems { get; init; }
}
```

For instance, consider the typical procedures related to an order: verifying warehouse inventory, processing shipments, and handling cancellations.

### Is it necessary to confirm inventory before processing payment?

That hinges on specific business rules.
```csharp
public partial class Order
{
    // Validated
    public bool IsValidated { get; init; }
    public decimal? Price { get; init; }
}
```
### Can we ship an order prior to receiving payment?

Some retailers permit payment upon delivery, especially for local shipments.

```csharp
public partial class Order
{
    // Shipped
    public bool IsShipped { get; init; }
    public string? TrackingUrl { get; init; }
}
```

### And what if a delivered order needs to be canceled?

Perhaps our system requires a more nuanced status than merely "canceled." A "dispute" status might be more apt, prompting further inquiries: Was the item damaged en route? Was there an error at the warehouse? Or did the customer mistakenly receive what they actually ordered?

```csharp
public partial class Order
{
    // Cancelled
    public bool IsCancelled { get; init; }
    public string? CancellationReason { get; init; }
}
```

## Problem statement
```csharp
public class User
{
    // ...
    
    public IEnumerable<Order> Orders { get; init; }
}

public class Order
{
    // Validated
    public bool IsValidated { get; init; }
    public decimal? Price { get; init; }
    public IEnumerable<OrderItem> OrderItems { get; init; }

    // Shipped
    public bool IsShipped { get; init; }
    public string? TrackingUrl { get; init; }

    // Cancelled
    public bool IsCancelled { get; init; }
    public string? CancellationReason { get; init; }
}
```

The intricacies related to specific order states are only meaningful within their respective contexts.  In C#, the class design might not always explicitly indicate the relationship between an order's status and its content. This singular class attempts to capture diverse order states, leading to potential ambiguities. How can we better encapsulate these states and the associated business logic?

## Discriminated Unions

F# offers [discriminated unions (DU)](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/discriminated-unions) to address such challenges:

```fsharp
type ValidatedOrder = {
    Price: decimal
}

type ShippedOrder = {
    TrackingUrl: string
}

type CancelledOrder = {
    CancellationReason: string
}

type Order =
    | New
    | Validated of ValidatedOrder
    | Shipped of ShippedOrder
    | Cancelled of CancelledOrder
```

DUs are akin to enhanced enums in F#, allowing encapsulation of distinct states and their associated data. They're typically used with pattern matching for clarity.

```fsharp
let matchOrder order =
    match order with
    | Shipped s -> $"Tracking URL: {s.TrackingUrl}"
    | Cancelled c -> $"Reason: {c.CancellationReason}"
```    

## "Closed" Type Hierarchies

For those rooted in C#, the [`discriminated unions`](https://github.com/dotnet/csharplang/issues/113) proposal has been around for quite some time. Apparently, it's not going to
make it in C#12 mainly because of several important discussion threads:

- Proposal: [Add completeness checking to pattern matching draft specification roslyn #188](https://github.com/dotnet/roslyn/issues/188)
- Proposal: ["Closed" type hierarchies #485](https://github.com/dotnet/csharplang/issues/485)
- Discussion: [Unions/closed hierarchies/ADTs, which one does C# 7.1+ need & what would the spec look like? #75](https://github.com/dotnet/csharplang/issues/75)

The "[discriminated unions](https://github.com/dotnet/csharplang/issues/113)" and the "[closed type hierarchies](https://github.com/dotnet/csharplang/issues/485)" are both viable language design alternatives and potent implementation options for the `Order` class. In this specific case, we even don't need the hierarchy to be "closed" Let's check the difference between the DU and the hierarchy approaches:

```csharp
public abstract class OrderBase
{
    public IEnumerable<OrderItem> OrderItems { get; init; }    
}

public class ValidatedOrder : OrderBase
{
    public decimal? Price { get; init; }
}

public class ShippedOrder : OrderBase
{
    public string? TrackingUrl { get; init; }
}

public class CancelledOrder : OrderBase
{
    public string? CancellationReason { get; init; }
}
```

Coincidentally, that's how [EF core handles hierarchies by default](https://learn.microsoft.com/en-us/ef/core/modeling/inheritance).

## Control Flow

The biggest difference between these two implementation options is how the control flow is organized. Discriminated unions use pattern matching while closed type hierarchies rely on good, old polymorphism.

Some people will claim the former option is more "functional" while the latter is more "object-oriented". Practically speaking, C# `switch` statements/expressions are, unlike F# `match`, not exhaustive. One of the main advantages of "closed" hierarchies is that they give us a list of all derived types at the compile time. 

## Concluding Thoughts

Whether using Discriminated Unions in F# or exploring Closed Type Hierarchies in C#, the goal remains: clear, maintainable, and robust code that accurately represents business logic and reduces room for errors. Leveraging the best of both functional and object-oriented paradigms can lead to superior code quality.