Scala and pattern matching

on waitingforcode.com

Scala and pattern matching

At first glance Scala's pattern matching looks similar to Java's switch statement. But it's only the first impression because after analyzing the differences we end up with some smarter idea.

In this another post of One Scala Feature per week series we'll focus on the feature called pattern matching. In the first section we'll discover its definition. In the next section we will see its use cases. Finally, in the last part we will read about some reflex we often have in the first months of working with Scala.

Pattern matching definition

Pattern matching is a mechanism letting us to check some value against one or more defined pattern. The similarity with switch statement comes mainly from the grammar that looks like: matchedValue match { case SomeTypeToMatch => "xxx" case SomeOtherTypeToMatch => "yyy" case _ => "default value" }

As you can see, pattern matching is built of the matched value (matchedValue) and one or more cases against which the value is tested (case ... => "..."). It's a simple version of pattern matching. More advanced one uses the system of pattern guards that help to limit the conditions under which given case matches. The following snippet shows the use of guards used to detect if given number is even or odd, or if matched value is not an Int:

it("should apply with guards") {
  // Please note it's only for illustration purpose
  // Obviously, we could avoid an IllegalArgumentException by simply
  // transforming Any argument to an Int
  def getNumberLabel(nr: Any): String = {
    nr match {
      case number: Int if number % 2 == 0 => "even"
      case number: Int if number % 2 != 0 => "odd"
      case _ => throw new IllegalArgumentException("Test can be made only on integers")
    }
  }

  val labelFor2 = getNumberLabel(2)
  labelFor2 shouldEqual "even"
  val labelFor3 = getNumberLabel(3)
  labelFor3 shouldEqual "odd"
  val error = intercept[IllegalArgumentException] {
    getNumberLabel("text")
  }
  error.getMessage shouldEqual "Test can be made only on integers"
}

The above code snippet shows the use of guards that in fact are simple if statements applied on matched elements.

Pattern matching use cases

Pattern matching with guards looks like a switch statement after all. However it's not the case since it can do a lot more stuff, as matching only types, recursively match a nested data structure and so on. All interesting use cases, found in already quoted in the post about "Pattern matching in Scala", are listed below:

  • matching nested structures, aka deep matches - pattern matching can be used to make recursive calls on nested data structures to find, for instance, the first matching node (e.g. graph) or the root of analyzed structure (e.g. tree). But since it involves recursive calls, remember to put a break condition. An example of such use is presented below:
    it("should apply to deep matches") {
      // Use null instead of Optional for brevity
      case class Person(name: String, parent: Person = null)
      val peopleHierarchy = Person("A",
        Person("B",
          Person("C",
            Person("D",
              Person("E", null)
            )
          )
        )
      )
    
      def getRootParentName(person: Person): String = {
        val parentName = person match {
          case Person(_, parent) if parent != null => {
            getRootParentName(parent)
          }
          case Person(name, parent) if parent == null => s"${name}"
          case _ => throw new IllegalMonitorStateException("Neither children nor parent was found")
        }
        parentName
      }
    
      val parentName = getRootParentName(peopleHierarchy)
    
      parentName shouldEqual "E"
    }
    
  • type patterns - pattern matching can be used to replace isInstanceOf type checks:
    it("should apply to type patterns") {
      case class A()
      case class B()
      case class C()
    
      def testType(someObject: Any): String = {
        someObject match {
          case _: A => "class#A"
          case _: B => "class#B"
          case _: C => "class#C"
          case _ => "unknown class"
        }
      }
    
      val matchedClassName = testType(B())
    
      matchedClassName shouldEqual "class#B"
    }
    
  • sequence patterns - pattern matching is able to match against simple types but also against the values of a sequence where its head is separated from the tail by :: sign:
    it("should apply to sequence patterns") {
      val letters = Seq("a", "b", "c", "d")
    
      val sequenceHead = letters match {
        case firstLetter :: _ => firstLetter
        case _ => "unknown"
      }
    
      sequenceHead shouldEqual "a"
    }
    
  • exhaustiveness checks - Scala compiler is able to detect the cases not covered by the pattern matching clause. A great example of that is the situation when we match against an optional value and consider only the case of defined value:
    def testOptional(someObject: Option[Int]): Int = {
      someObject match {
        case Some(nr) => nr
      }
    }
    
    Above code produces a compilation warning:
    Warning:(82, 7) match may not be exhaustive.
    it would fail on the following input: None
         someObject match {
    
  • variable bindings - in addition to type tests we can also extract values of matched type (to get more information see the post about Scala extractors):
    it("should use variable bindings in matched clause") {
      case class Address(street: String, city: String, country: String)
    
      val testedAddress = Address("street#1", "city#1", "country#1")
    
      val city = testedAddress match {
        case Address(_, cityFromAddress, _) => cityFromAddress
        case _ => ""
      }
    
      city shouldEqual "city#1"
    }
    
    Similar case is present in comprehensions, as here for a triple:
    it("should be used for comprehensions") {
      val testedAddress = ("street#1", "city#1", "country#1")
    
      val (_, city, _) = testedAddress
    
      city shouldEqual "city#1"
    }
    
  • partial functions - pattern matching is also used in shortened partial functions versions:
    it("should be used as a partial function") {
      val letters = Seq("z", "a", "b", "c")
    
      val aLetter = letters.collectFirst {
        case letter if letter == "a" => "A"
      }
    
      aLetter shouldEqual Some("A")
    }
    

Risk of overuse

But the risk of so powerful pattern matching is its overuse. It's especially visible in the code of programmers using this mechanic for the first time. In such situation almost all if-else conditions are expressed as pattern matching cases. Is it bad ? Yes and not. For many pattern matching is more readable than if-else statement. For the others its use should be only limited to specific cases (e.g. extracting components from objects) and not to make conditionals checks on the same type values (after all guards contain an if too).

A good point helping to make a decision concerns code complexity. In the compiled bytecode, a simple if-else statement is much shorter than pattern matching even applied on only 2 branches:

def getOneOrAnotherIf(value: Int): Boolean = {
  if (value%2 == 0) true else false
}

def getOneOrAnotherPatternMatching(value: Int): Boolean = {
  value match {
    case nr if nr%2 == 0 => true
    case _ => false
  }
}

That in bytecode gives:

  public boolean getOneOrAnotherIf(int);
    Code:
       0: iload_1
       1: iconst_2
       2: irem
       3: iconst_0
       4: if_icmpne     11
       7: iconst_1
       8: goto          12
      11: iconst_0
      12: ireturn

  public boolean getOneOrAnotherPatternMatching(int);
    Code:
       0: iload_1
       1: istore_2
       2: iload_2
       3: lookupswitch  { // 0
               default: 12
          }
      12: iload_2
      13: iconst_2
      14: irem
      15: iconst_0
      16: if_icmpne     23
      19: iconst_1
      20: goto          24
      23: iconst_0
      24: ireturn

This section is not here to give you the final answer but only to emphasize the risk of pattern matching overuse - especially at the beginning. With the practice we should acquire the reflex about the cases when if-else is more adequate than pattern matching and inversely.

The introduction talked about similarities witch Java's switch case, at least at first approach. But as proven throughout all 3 sections, it's only superficial. Java's switch statement applies on specific values whereas Scala's pattern matching has much more use cases. They all were shown in the second section and among the most important ones we should list 3 kinds of matches: deep, type and sequence ones. Combined together they can help to build really powerful methods extracting some values of matched object.

Read also about Scala and pattern matching here: Pattern matching in Scala .

Share, like or comment this post on Twitter: