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 = MailboxProcessorlet ( 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 ->letrec 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 logicreturn! 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:

<div style="padding-bottom:0px;margin:0px;padding-left:0px;padding-right:0px;display:inline;float:none;padding-top:0px;" id="scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:f5361990-e6b4-468a-b01c-c4b3a2a24b04" class="wlWriterEditableSmartContent">member this.Add(item) = agent <span style="color:#000000;">