C# – In what way are union types better for correctness than a common interface

%fcfunctional programming

I've just recently started familiarising myself with functional programming, mostly via F#, and there's one particular functional idiom that I'm not fully understanding the benefits of. I've seen it described in a few places, but I'll refer to the version in this article.

The article describes a shopping cart with several requirements, which essentially put it in one of three states: Empty, Active (occupied by one or more items but not yet paid for), or PaidFor. It's argued that by using F#, it's easy to ensure correctness because the compiler will prevent you from performing illegal operations on cart states. For example:

let addItemToCart cart item =  
   match cart with
   | Empty state -> state.Add item
   | Active state -> state.Add item
   | PaidFor state ->  
       printfn "ERROR: The cart is paid for"
       cart   

Only the Empty and Active states have Add methods attached, so if we tried to instead write:

| PaidFor state -> state.Add item

or similar, we'd get a compiler error.

By comparison, an OO approach might (in C#) look like:

interface ICart
{
    ICart AddItem(CartItem item);
}

class EmptyCart : ICart
{
    public ICart AddItem(CartItem item)
    {
        return new ActiveCart(item);
    }
}

class ActiveCart : ICart
{
    public ICart AddItem(CartItem item)
    {
        return new ActiveCart(_items.Add(item));
    }
}

class PaidForCart : ICart
{
    public ICart AddItem(CartItem item)
    {
        throw new InvalidOperationException("Who cares about Liskov anyway?");
    }
}

What I'm trying to understand is what benefit a client gets from having addItemToCart rather than ICart.AddItem. Either way, there's no compile-time check stopping the client from using that function with a paid for cart. And either way, the client has no control over what happens in the error case.

The functional version could be modified to give some control to the client over what happens in the error case (a callback, or some wrapper type over the result), but the object-oriented could be modified to do so just as easily (maybe with a TryAddItem method, which would be a bit more OO-idiomatic).

So what am I missing?

Edit

It was mentioned that in the comments that in the C# version, anyone could create an ICart implementation. To address that: an alternative version would be to replace ICart with an abstract Cart class and make its constructors internal. This isn't ideal and in some situations may not be feasible, but I think in the general case it works pretty well.

Best Answer

There is no benefit from from having addItemToCart rather than ICart.AddItem. They are essentially identical. Both of those necessarily have a runtime check, because you want to be able to have a variable with any kind of Cart in it, and you won't know at compile time which one will get passed into the function.

Where the benefit comes is when you first implement addItemToCart, or later when you add another operation at the same level of abstraction, say oneClickAddAndPay. The union type will give you a compile error if you try to Add an item to a PaidFor cart, or if you completely forget to account for PaidFor carts at all. The ICart interface can't catch that kind of programming error at compile time.

In other words, the union type can't move all kinds of errors to compile time, but it does move some. If you don't end up adding a lot of functions like oneClickAddAndPay that reuse the existing Add functionality, it won't buy you much.