5

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.

  • 1
    From the article, the compile time error is for paying, which is arguably the dangerous operation. What's more, your C# code isn't equivalent because an interface allows infinite implementations; you can't add new classes to the F# Cart type, ensuring that you only ever deal with the three cases. The equivalent C# code would have to use an abstract class with a private constructor and sealed inner subclasses to simulate the finite number of alternatives, and a visitor to safely determine which which alternative you're dealing with. It gets verbose/ugly really, really fast. – Doval Nov 19 '14 at 19:31
  • Note that interfaces and union types have dual properties. An interface makes it easy to introduce new alternatives (the implementing classes) but you have to keep the interface methods fixed, and you can't inspect other instances of the interface. With a union type the alternatives are fixed but adding new functions isn't a problem, and you can have binary functions that look at the fields of two instances of the union type. – Doval Nov 19 '14 at 19:39
  • @Doval I understand that the two aren't exactly equivilent, but I'm not sure how this is relevant to the point. So what if somebody could add more ICart implementations? What's the problem? And the point of my question is that I don't see why the Visitor-type approach (which is much closer to a literal translation of the functional version) is any better than the naive OO approach I described in the question. – Ben Aaronson Nov 19 '14 at 19:42
  • 2
    How will you prove your eShop software does the right thing every time when anyone can make a bogus ICart that doesn't remove items when requested, or adds items three times, or lets you pay for carts that have already been paid? The naive OO approach you presented leaves much more room for error, which is bad when correctness is critical, and the Visitor approach is much more cumbersome. – Doval Nov 19 '14 at 19:50
  • Won't the Add blow up at compile time in the F# version? A better implementation in C# would be to have two interfaces, one with Add and one without. – Telastyn Nov 19 '14 at 19:53
6

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.

  • This is a good point. But comparing like for like, I'm still not exactly sure I see the benefit. When I implement the empty cart case in addItemToCart, I do it in the match statement. When I implement it for ICart.AddItem, I do it in the body of EmptyCart.AddItem. Why is one of those any safer than the other? – Ben Aaronson Nov 19 '14 at 22:18
  • 1
    You're not implementing the entire empty cart case in the match expression. Part of it is in the match expression, and part of it is in the addToEmptyState function. You have two layers of abstraction instead of one, and calls from the upper to the lower can now be type checked at compile time. Like I said, it doesn't buy you much unless you do a lot more work in the upper layer than the example shows. – Karl Bielefeldt Nov 19 '14 at 22:41
2

In your example I see no benefit, but other languages (like Ceylon) use union types in useful ways. Imagine doing something like this (C#)

int Length<A> (String | List<A> obj) {...}

While this example isn't very meaningful since String implements IEnumerable, the idea behind this is that you can receive a parameter which can be one various type, use the fields/properties/methods those types have in common naturally, and use switch/case when type dependent behavior is needed.

In F# I guess you could create some helper types to accomplish little things like this. I prefer the Ceylon way: Polymorphism + (mainly anonymous) Union Types.

Union Types can help a lot for correctness if you assume that null is the only instance of type Null, then if you want to declare that something can be null you should use the type T | Null or its abbreviation T?.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.