Scala Tutorials Part #23 - Pattern Matching in Scala


Pattern Matching

This is part 23 of the Scala tutorial series. Check here for the full series.

Index

Pattern matching value types

We saw how extractors can aid pattern matching by writing an unapply method. In this article, we are going to see how they actually work and also look at the internals.

Let’s say we have an Integer variable and want to do a match on it.

 val status  = 0

 status match {
   case 0 => println("The status is false")
   case 1 => println("The status is true")
   case _ => println("Unknown status")
 }

Code is pretty self explanatory, it checks for matching values 0/1 and prints appropriate messages. The _ is used to match any other value, kind of like the default in java switch statements.

That was straightforward, let’s try with a double.

  val score = 4.0

  score match {
    case 4.0 => println("High score")
    case 3.0 => println("Moderate")
    case 2.0 => println("Low")
    case 1.0 => println("Very low")
    case _ => println("Unknown score")
  }

It is more or less similar to the Int example. In a real world situation we will have the need to use conditional expressions to match one or more cases.

The left side of the expression is used to capture the variable and also the condition matching it while the right side i.e the expression after the => returns a value. In the above example it returns a Unit since it just prints out and does not do any other computation.

If the execution does not match any of the cases, then it simply throws an exception.

object RunExample extends App{
  
  val status  = 10

  status match {
    case 0 => println("The status is false")
    case 1 => println("The status is true")
  }
  
}

Executing the above code results in the following.

Exception in thread "main" scala.MatchError: 10 (of class java.lang.Integer)
  at RunExample$.delayedEndpoint$RunExample$1(RunExample.scala:8)
  at RunExample$delayedInit$body.apply(RunExample.scala:3)
  at scala.Function0$class.apply$mcV$sp(Function0.scala:34)
  at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12)
  at scala.App$$anonfun$main$1.apply(App.scala:76)
  at scala.App$$anonfun$main$1.apply(App.scala:76)
  at scala.collection.immutable.List.foreach(List.scala:381)
  at scala.collection.generic.TraversableForwarder$class.foreach(TraversableForwarder.scala:35)
  at scala.App$class.main(App.scala:76)
  at RunExample$.main(RunExample.scala:3)
  at RunExample.main(RunExample.scala)

Using conditionals

Taking the same example above with a broader score range.

 val score = 8.0

  score match {
    case highScore 
      if highScore >= 8.0 && highScore <= 10.0 => 
      println("High score")
    case averageScore 
      if averageScore >= 5.0 && averageScore < 8.0 => 
      println("Average score")
    case lowScore 
      if lowScore >=0.0 && lowScore < 5.0 => 
      println("Low score")
    case _ => 
      println("Error. Invalid score. It has to be in the range 0.0 to 10.0")
  }

The variables highScore, averageScore and lowScore are actually doubles and can be used in the right side of the computation.

  val score = 9.0

  score match {
    case highScore
      if highScore >= 8.0 && highScore <= 10.0 =>
      println(s"High score : Got $highScore")
    case averageScore
      if averageScore >= 5.0 && averageScore < 8.0 =>
      println(s"Average score : Got $averageScore")
    case lowScore
      if lowScore >=0.0 && lowScore < 5.0 =>
      println(s"Low score : Got $lowScore")
    case _ =>
      println("Error. Invalid score. It has to be in the range 0.0 to 10.0")
  }

Since java switch case statements can take only constant values in its case’s, this is more elegant to work with. It is important to note that there is no need of a break statement since it automatically matches only of the cases present and falls back to the _ case if there is no match and throws an exception if there is no fallback as we saw above. Conditionals are also called guard statements similar to the guard in for comprehensions.

Pattern matching strings

So far we have been seeing value types. Let’s take a look at string pattern matching which is very useful(Intentionally keeping the examples simple in order to grasp the concepts).

  val dayOfTheWeek = "Sunday"

  dayOfTheWeek match {
    case "Sunday" => println("Holiday")
    case "Saturday" => println("Holiday")
    case "Monday" => println("Weekday")
    case "Tuesday" => println("Weekday")
    case "Wednesday" => println("Weekday")
    case "Thursday" => println("Weekday")
    case "Friday" => println("Weekday")
    case _ => println("Invalid Day")
  }

If we want the comparison to be case-insensitive then,

  val dayOfTheWeek = "monday"

  dayOfTheWeek toLowerCase match {
    case "sunday" => println("Holiday")
    case "saturday" => println("Holiday")
    case "monday" => println("Weekday")
    case "tuesday" => println("Weekday")
    case "wednesday" => println("Weekday")
    case "thursday" => println("Weekday")
    case "friday" => println("Weekday")
    case _ => println("Invalid Day")
  }

Locale should be handled correctly in the above example, but you get the idea.

We can optimize the above code block into something more concise as below,

val dayOfTheWeek = "sunday"

  dayOfTheWeek toLowerCase match {

    case "sunday" | "saturday" =>
      println("Holiday")

    case
      "monday"  | "tuesday" | "wednesday" | "thursday" | "friday" =>
      println("Weekday")

    case _ => println("Invalid day of the week")
  }

The | is a shorthand for or condition.

Capturing values

Each match in a pattern match block are capable of returning a value and hence the result can be stored into a variable.

val score = 8.0

  val scoreFeedback = score match {
    case highScore
      if highScore >= 8.0 && highScore <= 10.0 =>
      "High score"
    case averageScore
      if averageScore >= 5.0 && averageScore < 8.0 =>
      "Average score"
    case lowScore
      if lowScore >=0.0 && lowScore < 5.0 =>
      "Low score"
    case _ =>
      "Error. Invalid score. It has to be in the range 0.0 to 10.0"
  }

Matching with Options

In case we do not want to store the error message in the result, then we can use an Option.

val scoreFeedback : Option[String] = score match {
    case highScore
      if highScore >= 8.0 && highScore <= 10.0 =>
      Some("High score")
    case averageScore
      if averageScore >= 5.0 && averageScore < 8.0 =>
      Some("Average score")
    case lowScore
      if lowScore >=0.0 && lowScore < 5.0 =>
      Some("Low score")
    case _ =>
      println("Error. Invalid score. It has to be in the range 0.0 to 10.0")
      None
  }

The results can then be pattern matched again as we saw in part 16.

Heterogeneous pattern matching

Pattern matching is not restricted to a particular type. Because of Scala’s robust type system, we can literally match anything that fits the type hierarchy properly.

  val monthOfYear : Any = "January"

  monthOfYear match {
    case 1 | "January" => println("First month of the year")
    case 2 | "February" => println("Second month of the year")
    case _ : Int => println("Invalid month integer")
    case _ : String => println("Invalid month string")
  }

Decomposing types using pattern matching

Another unique capability of pattern matching is to decompose an unknown type or higher type into a recognized type.

  val typeTest : Any = "String"

  typeTest match {
    case i : Int => println("Integer type")
    case d : Double => println("Double type")
    case f : Float => println("Float type")
    case s : String => println("String type")
    case _ : BigDecimal => println("Big decimal type")
    case _ => println("Unknown type")
  }

The type ascription Any is necessary in order for the compiler to treat it as a higher type and avoid the variable type being automatically inferred to String. In real world, the type you are going to match might come from an API endpoint/from a file etc.,

Case class matching

Case classes are named after pattern matching i.e the case keyword. They are naturally suited to it because of the unapply method which gets automatically generated.

Let’s create a textbook example of cars.

  abstract class Car
  case class Hyundai(name:String) extends Car
  case class Toyota(name:String) extends Car
  case class Audi(name:String) extends Car

Let’s create an instance of this car (the type ascription is important)

val car : Car = Audi("Audi V8")

We can then do a match on this variable.

 car match {
    case Hyundai(name) => println(s"$name is from South Korea")
    case Toyota(name) => println(s"$name is from Japan")
    case Audi(name) => println(s"$name is from Germany")
  }

We do a pattern match on the case class type with a parameter name. The parameter name is important since the case class cannot be created without it. You can revisit the decompiled version of the case class and see the unapply method. It would make much more sense now and how it is useful in pattern matching. Case objects can also be matched using a similar approach.

Pattern matching decompiled

We are not going to exhaustively see how pattern matching works behind the scenes for all examples. Let’s take three different examples to understand how they behave. Since the decompiled code is pretty big, I have collected both in a gist.

Fernflower decompiler does a pretty good job of decompiling the code. It is interesting to see that in the first example it is being compiled to java switch statements, the second one with a bunch of if-else since switch case does not support condition based matching. The third one is done via a couple of instanceOf comparisons. We can see that pattern matching gives us a very nice abstraction and lets the compiler deal with the all the hardwork. The decompiled code can change as the JVM evolves and a lot of features are added natively.

Finale

Let’s summarize what we have seen till now.

There are more complex use cases of pattern matching in data structures such as Seq, List, Vector etc., I will cover those when we get to collections.

We are also protected from quite a few run time issues partly due to the type system of Scala and also how pattern matching by itself is designed. Next time whenever you see a switch case/complex if-else structure, think of re-writing it with pattern matching.


Tagged Under


Scala


Search this website...




Keeping up with blogs...

I blog occasionally and mostly write about Software engineering and the likes. If you are interested in keeping up with new blog posts, you should follow me on twitter where I usually tweet when I publish them. You can also use the RSS feed , or even subscribe via email below.

Feedio Subscribe


Share & Like

If you like this post, then you can either share/discuss/vote up on the below sites.



Thoughts ...

Please feel free to share your comments below for discussion

Blog comments powered by Disqus