Going Hybrid - Implementing a Shopping Cart in F# Tuesday, December 15, 2009

One persistent question that keeps coming up to me is how to merge functional programming techniques with object oriented techniques that many are used to. My usual reply is to talk about how functional programming affects your code, programming in the big, programming in the medium and programming in the small. What I mean by those terms is:

Programming in the large: a high level that affects as well as crosscuts multiple classes and functions

Programming in the medium: a single API or group of related APIs in such things as classes, interfaces, modules

Programming in the small: individual function/method bodies

Where functional programming has an immediate impact and probably the largest is programming in the small. Here, we can focus on such things as immutable values, higher order functions, recursion, pattern matching and others come into play. When we’re talking about mixing paradigms, object oriented programming has a larger effect on programming in the medium where we’re organizing our code and can some times offer a more elegant solution than a functional programming one. Functional languages also have a good effect in programming in the medium when object oriented solutions such as the visitor pattern, command pattern and others could be expressed more elegantly.

Since I presented at the Continuous Improvement in Software Development Conference last year, Jeremy D. Miller has often asked about using features of F# to create the canonical Shopping Cart solution. After a while of listening to this comment, I finally sat down and came up with a simple solution using both functional programming and object oriented ideas. This solution uses an underlying agent based model to maintain our shopping cart state through passing messages of our intention.

First, let’s get some basic functions and type aliases out of the way before we begin which will help us in our design:

type Agent < ' a> = MailboxProcessor< ' a > let ( <-- ) (agent:Agent < _ > ) msg = agent.Post(msg)

These above pieces allow me to change the MailboxProcessor class to an Agent and also have a function which posts a message which we’ll use later. Now, before we go any further, let’s talk about some typical states of a shopping cart and what kind of “state” is required to manage. Typically, we’d have the ability to add and remove items from our cart as well as the ability to check out for processing and payment. Keeping in mind of a simplistic model, we could model our semi-realistic state classes as such:

type Cart = { Total : decimal Items : Item list } and Item = { Name : string Quantity : int Price : decimal }

We have our Cart record which holds our total price and the list of items we which to purchase. The Item record holds the name of the product, the quantity and the price. Since we’re maintaining this internal “state” we need some way to signal to our system of our intentions, whether it is to add an item, remove an item, clear our cart, or check out. We might do that through a discriminated union to define our messages.

type CartMessage = | Add of Item | Remove of Item | Clear | Checkout

Above, we defined our messages of Add, Remove, Clear and Checkout. For both Add and Remove, I need an Item to add or remove from our cart. Now that we have some of the inner workings defined, let’s now go about creating our agent. First, let’s add a couple of helper functions which helps us calculate the total price and one that removes an item from the list.

type ShoppingCartAgent() = let calculateTotal items = (0m, items) ||> List.fold ( fun acc item -> acc + (item.Price * decimal item.Quantity)) let remove item = List.filter (( <> ) item)

Next, inside this class, we need to define our agent and what actions we take based upon the messages we receive. Let’s look at a simplistic view of how we might do that:

let agent = Agent.Start( fun inbox -> let rec loop (cart:Cart) = async { let ! msg = inbox.Receive() match msg with | Add item -> let items = item :: cart.Items let total = calculateTotal items return ! loop { cart with Total = total; Items = items } | Remove item -> let items = cart.Items |> remove item let total = calculateTotal items return ! loop { cart with Total = total; Items = items } | Clear -> return ! loop { cart with Total = 0m; Items = [] } | Checkout -> // Some logic return ! loop { cart with Total = 0m; Items = [] } } loop { Total = 0m; Items = [] })

Looking at the above code, we create our agent by calling the Agent.Start method which gives us our inbox that we can receive messages. Inside the Start method, we create an infinite loop which initializes our “state” with a new Cart record with default data. Inside of our loop, we receive a message which we pattern match against in order to take the appropriate action. In the case of Add, we add the item to the head of our list, recalculate the total and return a loop of our new state. For remove, the logic is much the same, except instead of adding the item, we remove it, recalculate our price and return our new state. The Clear case is rather self explanatory, so that really doesn’t need to be covered. Our Checkout case could be any number of things and not really the heart of what I’m proving here. It could be any number of things such getting the customer information on the Checkout message and then passing it along to another agent for processing.

Finally, to wrap things up, we need a way to encapsulate our agent as it doesn’t need to be exposed to the outside world. In order to do that, we simply create methods on our ShoppingCartAgent class to expose the functionality of Add, Remove, Clear and Checkout like the following:

member this.Add(item) = agent <-- Add item member this.Remove(item) = agent <-- Remove item member this.Clear() = agent <-- Clear member this.Checkout() = agent <-- Checkout

And now we have a complete agent system that handles a single customer and uses asynchronous messaging on the back end to manage our “state”.

Conclusion

This of course is one of the 10,000 ways I could have modeled the canonical Shopping Cart example, but in this case, I used functional programming techniques to use immutability, recursion, higher-order functions and pattern matching for programming in the small and medium, and using object oriented techniques to encapsulate our agent. Pragmatic multi-paradigm Languages such as F# and Scala afford us these opportunities to meld both together in an elegant solution.