Pretty image
In this seventh installment of his series on Scala, Venkat shows how Scala does pattern matching.

You’ve seen quite a few examples of Scala’s expressive and concise nature in this series. Pattern matching is another area where you’ll appreciate the elegance of Scala. When programming you constantly make decisions based on the types of data and their values. Scala provides simple yet powerful facilities to program this common operation, as you’ll see in this article.

To explore the expressive power of pattern matching, let’s write a function that will count the number of nickels, dimes, and quarters in a given collection:

 val coins = List(25, 10, 5, 10, 5, 25, 10, 5, 5, 25)

Using the traditional if-else construct, the code would look like this:

 def countCoins(coins : Seq[Int], nickels : Int = 0,
  dimes : Int = 0, quarters : Int = 0) : Map[String, Int] = {
 
  if (coins.isEmpty)
  Map("nickels" -> nickels, "dimes" -> dimes,
  "quarters" -> quarters)
  else {
  if (5 == coins.head) {
  countCoins(coins.tail, nickels + 1, dimes, quarters)
  }
  else {
  if (10 == coins.head) {
  countCoins(coins.tail, nickels, dimes + 1, quarters)
  } else {
  if (25 == coins.head) {
  countCoins(coins.tail, nickels, dimes, quarters + 1)
  } else {
  countCoins(coins.tail, nickels, dimes, quarters)
  }
  }
  }
  }
 }
 println(countCoins(coins))
 //Map(nickels -> 4, dimes -> 3, quarters -> 3)

The function accepts a collection of coins as the first parameter, followed by the known number of nickels, dimes, and quarters, all set to 0 by default. Within the method, if there are no coins, you return a map of the known counts. Otherwise, you check the first coin using a series of simple if-else statements and recursively process the rest of the coins. You could have used the collection’s filter method for this also, but in this example, we’ll see how to use patterns instead.

That’s quite a bit of code and it appears noisy. But we can use pattern matching to make this function concise. Scala provides a match method that appears something like a switch statement, but is more powerful. It is an expression that can filter or match the target object against different types and data values. The previous code will reduce to a few lines that are easier to follow.

 def countCoins(coins : Seq[Int], nickels : Int = 0,
  dimes : Int = 0, quarters : Int = 0) : Map[String, Int] = {
 
  if (coins.isEmpty)
  Map("nickels" -> nickels, "dimes" -> dimes,
  "quarters" -> quarters)
  else coins.head match {
  case 5 => countCoins(coins.tail, nickels + 1,
  dimes, quarters)
  case 10 => countCoins(coins.tail, nickels,
  dimes + 1, quarters)
  case 25 => countCoins(coins.tail, nickels,
  dimes, quarters + 1)
  case _ => countCoins(coins.tail, nickels,
  dimes, quarters)
  }
 }
 println(countCoins(coins))
 //Map(nickels -> 4, dimes -> 3, quarters -> 3)

Here you replaced the outermost else block with a call to the match method on the first element or the head of the coins collection. Scala matches the value of the first element with each case of literals you have provided and takes the appropriate route.

The underscore in case _ represents a wildcard, used if all of the other cases fail to match. Scala does not insist that you provide the wildcard case, but without it any mismatch would result in a runtime exception.

The match operation does not restrict you to simple literals. You can replace the top level if with a match in the coins count example:

 coins match {
  case Seq() =>
  Map("nickels" -> nickels, "dimes" ->
  dimes, "quarters" -> quarters)
  case _ =>
  coins.head match {
  case 5 => countCoins(coins.tail, nickels + 1,
  dimes, quarters)
  case 10 => countCoins(coins.tail, nickels,
  dimes + 1, quarters)
  case 25 => countCoins(coins.tail, nickels,
  dimes, quarters + 1)
  case _ => countCoins(coins.tail, nickels, dimes, quarters)
  }
 }

The first case takes effect if the collection is empty, and the wildcard case takes effect otherwise. You used two levels of match in this example, but that’s not required. The list match also permits you to probe into the contents of the list. Using this you can let the top level match check the values of the list and eliminate the nested match.

  coins match {
  case Seq() =>
  Map("nickels" -> nickels, "dimes" ->
  dimes, "quarters" -> quarters)
  case Seq(5, _*) => countCoins(coins.tail, nickels + 1,
  dimes, quarters)
  case Seq(10, _*) => countCoins(coins.tail, nickels,
  dimes + 1, quarters)
  case Seq(25, restOfTheCoins @ _*) =>
  countCoins(restOfTheCoins, nickels, dimes, quarters + 1)
  case _ => countCoins(coins.tail, nickels, dimes, quarters)
  }
 }

The pattern Seq(5, _*) matches a list whose first element is a value 5. The remaining elements in the list are represented by _* and may be captured into a reference, as in the case for 25. Capturing the values is optional, as you see in the case expressions.

But so far we’ve just scratched the surface of pattern matching. Scala allows you to match based on values and types; you can even use guarded cases using if, as you see next.

 def process(message : Any) = {
  message match {
  case "hello" => println("received hello")
  case x : String => println("received a string " + x)
  case (a, b) => println("received tuple (" + a + ", " + b + ")")
  case 22 => println("received 22")
  case x : Int if x < 0 => println("received a negative number " + x)
  case y : Int => println("received number " + y)
  }
 }
 process(1, 2) // received typle (1, 2)
 process("hello") // received hello
 process("test") // received a string test
 process(22) // received 22
 process(12) // received number 12
 process(-32) // received a negative number -32

The patterns "hello" and 22 look for an exact match. The patterns that specify the type, like x : String and y : Int, will accept any value of the appropriate type. The guarded case expects the value to satisfy the pattern in the case, type Int in this example, and the if statement to evaluate to true. You can also match tuples, which are lightweight immutable collections of values. In this example, you match any tuple with two values of any type. You can also dictate the type, like so: (a : Int, b : Double, c : String).

The order in which you place the case expressions matters. For example, if you reverse the last two cases, Scala will raise an unreachable code compile-time error. In a sense the pattern matching in Scala is like a coin sorter: the data falls into a matching slot as it flows over the cases.

You can match against various values and types, including your own types. Scala’s special type of classes named case classes are very useful for this. The instances of case classes are immutable and they allow easy matching and extraction of data, as in the next example.

 case class Sell(symbol : String, quantity : Int)
 case class Buy(symbol : String, quantity : Int)
 def processTrade(trade : Any) {
  trade match {
  case Sell(symbol, quantity) =>
  println("Selling " + quantity + " stocks of " + symbol)
  case Buy(symbol, quantity) if (quantity > 1000) =>
  println("Buying lots..." + quantity + " stocks of " + symbol)
  case Buy(symbol, quantity) =>
  println("Buying " + quantity + " stocks of " + symbol)
  }
 }
 processTrade(Sell("AAPL", 200)) // Selling 200 stocks of AAPL
 processTrade(Buy("AAPL", 2000)) // Buying lots...2000 stocks of AAPL
 processTrade(Buy("GOOG", 200)) // Buying 200 stocks of GOOG

When you create an instance of a case class, the values for the parameters flow in. For example, Sell("AAPL", 200) sends AAPL for the symbol and 200 for the quantity. When you use a case class in a pattern match, the values flow out. For example in case Sell(symbol, quantity), the values present in the instance of case class are extracted into these parameters. So, if the case class contained GOOG and 200 for these two fields, these values are extracted into the variables s and q, respectively, if the pattern is defined as case Sell(s, q). This direction in which the data moves is referred to as apply (invoking a function and sending data to it) and unapply (extracting the data from the object, moving the data in the opposite direction). The unapply operation is quite useful in pattern matching, as you’ll see next.

If you want to perform more powerful extractions and apply a series of pattern matching, you can use Scala extractors. In the next example you’ll write an extractor to parse the details of a task. First define a Task singleton class with an unapply method.

 object Task {
  def unapply(taskInfo : String) = {
  val parts = taskInfo.split("---")
  if(parts.size != 2) None else Some(parts(0), parts(1))
  }
 }

The unapply method expects the given string to contain one delimiter ---, and if the parameter conforms to that, returns as a tuple the two parts before and after this delimiter. If the format does not conform, it returns a None, indicating a failure of extraction. The unapply method can signal a failure by returning either a None or a false.

The values in the tuple returned from the unapply will be assigned to individual parameters you place in the case. You can use the Task as an extractor.

 def processTask(detail : String) = {
  detail match {
  case Task(day, task) => println("Task for " + day + ":" + task)
  case _ => println("invalid task format")
  }
 }

If the detail parameter conforms to the format that the Task extractor expects, the day and task parameters will contain the day and the task item for the day.

Exercise this function with a few calls:

 processTask("Monday --- integrate with the payment system")
 //Task for Monday : integrate with the payment system
 processTask("Wednesday -- hack code with no tests")
 //invalid task format
 processTask("Frday --- discuss discount rates")
 //Task for Frday : discuss discount rates

In the first call, the task conformed to the format and the extractor provided you the day and the task. In the second call, you ended up with a format error since the parameter did not conform to the expected format. The last call has the right format, but the day of week is messed up. This went undetected, but it’s really easy to fix.

Create a DayOfWeek extractor to check the day of the week. The unapply method for this extractor can simply return a true or false to indicate if the string matches a day of the week.

 object DayOfWeek {
  def unapply(day : String) = {
  List("Sunday", "Monday", "Tuesday", "Wednesday",
  "Thursday", "Friday", "Saturday") contains day.trim
  }
 }

Let’s improve the processTask method to use this extractor in combination with the extractor we already have.

 def processTaskImproved(detail : String) = {
  detail match {
  case Task(day @ DayOfWeek(), task) =>
  println("Task for " + day + ":" + task)
  case _ => println("invalid task day or format")
  }
 }

In the first case expression you first invoke the Task extractor. This parses the content and splits the day part from the task information part. You further invoke the DayOfWeek extractor on the day part. The value of day is stored in the day reference but the case match is not completed until the DayOfWeek extractor signals a success. If the day matches the format for a day of the week, as indicated by the unapply method of DayOfWeek, then the pattern match succeeds. If either the Task extractor signals a failure by returning None or the DayOfWeek extractor signals a failure by returning false, the match will not succeed. Exercise this improved version with the same arguments as you have sent to the first version of processTask and ensure that the validation of the data also happens.

 processTaskImproved("Monday --- integrate with the payment system")
 // Task for Monday : integrate with the payment system
 processTaskImproved("Wednesday -- hack code with no tests")
 // invalid task day or format
 processTaskImproved("Frday --- discuss discount rates")
 // invalid task day or format

In this article we continued our tour of Scala and looked at how Scala provides powerful pattern matching and extraction without compromising a bit on its expressive and concise nature. In the next part in this series, we’ll explore a soft nature of Scala—the love it gives to something most of us have come to dread, XML processing.

Dr. Venkat Subramaniam is an award-winning author, founder of Agile Developer, Inc., and an adjunct faculty at the University of Houston.

He has trained and mentored thousands of software developers in the US, Canada, Europe, and Asia, and is a regularly invited speaker at several international conferences. Venkat helps his clients effectively apply and succeed with agile practices on their software projects.

Venkat is the author of .NET Gotchas, the coauthor of 2007 Jolt Productivity Award winning Practices of an Agile Developer, the author of Programming Groovy: Dynamic Productivity for the Java Developer and Programming Scala: Tackle Multi-Core Complexity on the Java Virtual Machine. His latest book is Programming Concurrency on the JVM: Mastering Synchronization, STM, and Actors.

Send the author your feedback or discuss the article in the magazine forum.