This post is over 6 months old. Some details, especially technical, may have changed.

Refactoring Pattern Matching in Scala

I want to take a look at making effective use of Scala's pattern matching capabilities for a fairly trivial example. Don't expect any of this to be mind blowing but maybe you're venturing in Scala for the first time or have been tinkering for a while you might find this useful.

The example I give below has been taken from a real world example but the context has been changed and made bit more trivial and non-specific.

Setting the stage

We have a solution by which a customer can place an order through our system. They can pay by various means - Credit Card, Bitcoin and Direct Debit. Once they have placed their order they are taken to a payment selection screen where they can select how they want to pay. Finally, once they have selected their payment method they are redirected to the appropriate payment gateway.

First lets model the PaymentMethod types

sealed trait PaymentMethod

object PaymentMethod {
  case object Card extends PaymentMethod
  case object Bitcoin extends PaymentMethod
  case object DirectDebit extends PaymentMethod
}

This gives us case objects that represent the payment types our system supports. Next lets create an Order class to model our actual order.

case class Order(id: String, total: BigDecimal, selectedPaymentMethod: Option[PaymentMethod])

This class has some properties relating to the order

  1. An id property, and
  2. A total property that holds the final price of the order.

It also has the selectedPaymentMethod property which is an option of our PaymentMethod type. It is defined as an Option because prior to payment selection we will have an order but no payment method selected.

Now lets assume our system is an event based system. Once payment select has been made the Order instance is persisted to a data store with the chosen option and a system event is triggered to being processing payment and routing the customer.

Within whatever event handling strategy we use (Actor, Queue, Bus, Callback etc.) we then need to retrieve the customers most recent order and if there is a payment method defined route to the correct payment gateway. We have defined a method like for retrieving the mot recent customer order,

def mostRecentCustomerOrder(customerId: String): Option[Order]

It returns a Option[Order] because there are situations where this is used and the customer doesn't yet have a recent order. Just go with it.

Decisions using pattern matching

Now to the juicy bit. Given we've got the most recent order we need to route accordingly. At this point we want to return yet another Option this time of a potential Route which is an abstraction that allows us to route to external systems (the details are really not important for this example).

  • If there is no order we return None
  • If there is an order but no selected payment method we return None
  • If there is an order and a selected method we return a Some of a route depending on what the select method is.

Lets code this up.

mostRecentCustomerOrder(customerId) match {
  case Some(order) if order.selectedPaymentMethod == Some(PaymentMethod.Card) =>
    Some(redirectToPaymentGateway())
  case Some(order) if order.selectedPaymentMethod == Some(PaymentMethod.Bitcoin) =>
    Some(redirectToBitcoinGateway())
  case Some(order) if order.selectedPaymentMethod == Some(PaymentMethod.DirectDebit) =>
    Some(redirectToBankGateway())
  case _ => None
}

I see this code a lot. It's not necessarily wrong but it is rather long winded. We are doing some things unnecessarily. For example, we are extracting the Order into an order value and inspecting the contents. Not a major issue but we aren't actually using it inside the case statements body. We also run the risk of shadowing other values of the same name.

Deeper matching

Pattern matching in Scala isn't confined to the top level class. It is entirely possible to match all the way down the object graph. So lets revisit the same bit of code and remove the unnecessary extraction of a value and just use pure matching.

mostRecentCustomerOrder(customerId) match {
  case Some(Order(_, _, Some(PaymentMethod.Card))) =>
    Some(redirectToPaymentGateway())
  case Some(Order(_, _, Some(PaymentMethod.Bitcoin))) =>
    Some(redirectToBitcoinGateway())
  case Some(Order(_, _, Some(PaymentMethod.DirectDebit))) =>
    Some(redirectToBankGateway())
  case _ => None
}

In this case we avoid extracting the order value and make each case a bit more succinct by matching purely on the existence of a PaymentMethod within the Option. We also use _ to mark values we aren't interested in. Whether or not this is easier to read than the previous example is very subjective. I think it is but only really marginally so.

Option.collect

There is another option (pun intended). As mostRecentCustomerOrder returns an Option and we are expected to return an Option we can use Option.collect.

collect[B](pf: PartialFunction[A, B]): Option[B]
Returns a scala.Some containing the result of applying pf
to this scala.Option's contained value, if this option is
nonempty and pf is defined for that value.

This gives us the power of pattern matching so we can transform the input Option in to what we want if and only if the input satisfies our conditions, falling back to None if it doesn't. You can think of collect like a conditional map

mostRecentCustomerOrder(customerId).collect {
  case Order(_, _, Some(PaymentMethod.Card)) =>
    redirectToPaymentGateway()
  case Order(_, _, Some(PaymentMethod.Bitcoin)) =>
    redirectToBitcoinGateway()
  case Order(_, _, Some(PaymentMethod.DirectDebit)) =>
  	redirectToBankGateway()
}

By using collect we don't need to provide the default fallback case of returning None plus we get to unwrap all those Some(Order(...)) cases as well as not having to wrap each cases return value in a Some.

Again this is purely personal opinion but this reduction of noise makes this code much easier to read. We are letting the language deal with the default cases without having to repeat ourselves.

Summary

None (pun intended) of the options (pun intended) above are necessarily wrong and one approach may fit better in some scenarios than another but I feel that laying out the options (pun intended) helps us make more informed decisions while we try and build quality code.

Published in Scala on December 21, 2015