Going Hybrid – Implementing a Shopping Cart in F#

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.

This entry was posted in F#, Functional Programming, OOP. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • Manning Jersey

    For CXHqwwe1213 without Cheapest NFL Jerseys words, in friendship, Cheap Nike NFL Jerseys all thoughts, all desires, NFL Jerseys Online all expectations Cheap NFL Jerseys China are born and shared, Von Miller Jersey with joy that is unacclaimed.

  • Manning Jersey

    For CXHqwwe1213 without [url=http://www.cheapestnfljerseysonline.com/][b]Cheapest NFL Jerseys[/b][/url] words, in friendship, [url=http://www.cheapestnfljerseysonline.com/][b]Cheap Nike NFL Jerseys[/b][/url] all thoughts, all desires, [url=http://www.cheapestnfljerseysonline.com/][b]NFL Jerseys Online[/b][/url] all expectations [url=http://www.cheapestnfljerseysonline.com/][b]Cheap NFL Jerseys China[/b][/url] are born and shared, [url=http://www.officialbroncosnflsales.com/][b]Von Miller Jersey[/b][/url] with joy that is unacclaimed.

  • http://codebetter.com/members/Matthew.Podwysocki/default.aspx Matthew.Podwysocki

    @Cesar,

    Yes, sumBy could have been used as well, and I guess I should have. As for the calcuateTotal in the first place, sure you could remove it and it should just work as well.

    Matt

  • http://www.kitiara.org Cesar Mendoza

    Thanks for another cool post. I have some questions:

    I know that this is just on of the 10,000 ways you could have implemented the chopping cart, but I would like to hear why did you use List.fold instead of List.sumBy in the implementation of calculateTotal?

    Also, let’s assume for the sake of an argument that the function calculateTotal is being slow because customer’s carts have long lists of items. Would it be OK to remove the calculateTotal function and use the Total that is on the Cart type to keep track of the total? In that case the code would look like this:

    | Add item ->
    let items = item :: cart.Items
    let total = cart.Total + (item.Price * decimal item.Quantity)

    return! loop { cart with Total = total; Items = items }

    | Remove item ->
    let items = cart.Items |> remove item
    let total = cart.Total – (item.Price * decimal item.Quantity)
    return! loop { cart with Total = total; Items = items }

    I think it’s OK because inbox.Receive() would serialize all the messages and avoid any race condition. Am I right or Am I missing something?

  • http://codebetter.com/members/Matthew.Podwysocki/default.aspx Matthew.Podwysocki

    @Evan,

    Thanks! I have plenty more samples of MailboxProcessor code if you look around the blog and feel free to ping me with any questions.

    Matt

  • http://www.evanhoff.com Evan

    Great post! I’ve spent some time over in Erlang doing a bit of learning, and I was beginning to wonder how spawn() and receive would translate (if at all) into F#/.NET. This post made a lot of things click for me..

  • http://codebetter.com/members/Matthew.Podwysocki/default.aspx Matthew.Podwysocki

    @Jeremy,

    You’re welcome. It’s one of 10,000 ways I could have solved the problem, as F# is a pragmatic language where I could have used mutable state if I wanted to, but I thought I’d tackle it from a much different angle.

    Matt

  • http://codebetter.com/members/jmiller/default.aspx Jeremy D. Miller

    Cool post, Matt, thanks for writing this. For the record though, Dot Net Rocks gets the blame for the “oh yeah, can you write a shopping cart with that?”