© Toby Weston 2018

Toby Weston, Scala for Java Developers, https://doi.org/10.1007/978-1-4842-3108-1_16

16. Pattern Matching

Toby Weston

(1)London, UK

As well as providing switch-like functionality (that’s more powerful than Java’s version), pattern matching offers a rich set of “patterns” that can be used to match against. In this chapter, we’ll look at the anatomy of patterns and talk through some examples, including literal, constructor, and type query patterns.

Pattern matching also provides the ability to deconstruct matched objects, giving you access to parts of a data structure. We’ll look at the mechanics of deconstruction: extractors, which are basically objects with the special method unapply implemented.

Switching

Let’s start by looking at the pattern match expression from earlier.

  val month = "August"
  val quarter = month match {
    case "January" | "February" | "March"    => "1st quarter"
    case "April" | "May" | "June"            => "2nd quarter"
    case "July" | "August" | "September"     => "3rd quarter"
    case "October" | "November" | "December" => "4th quarter"
    case _                                   => "unknown quarter"
  }

There are several key differences between Java’s switch and Scala’s match expression.

  • There is no fall-through behavior between cases in Scala. Java uses break to avoid a fall-through but Scala breaks between each case automatically.

  • In Scala, a pattern match is an expression; it returns a value. Java switches must have side effects to be useful.

  • We can switch on a wider variety of things with Scala, not just primitives, enums, and strings. We can switch on objects, and things, that fit a “pattern” of our own design. In the example, we’re using “or” to build a richer match condition.

Pattern matching also gives us the following:

  • The ability to guard the conditions of a match; using an if, we can enrich a case to match not only on the pattern (the part straight after the case) but also on some binary condition.

  • Exceptions for failed matches; when a value doesn’t match anything at runtime, Scala will throw a MatchError exception letting us know.

  • Optional compile-time checks: you can set it up so that if you forget to write a case to match all possible combinations, you’ll get a compiler warning. This is done using what’s called sealed traits.

Patterns

The anatomy of a match expression looks like this:

  value match {
      case pattern guard => expression
      ...
      case _             => default
  }

We have a value, then the match keyword, followed by a series of match cases. The value can itself be an expression, a literal or even an object.

Each case is made up of a pattern, optionally a guard condition, and the expression to evaluate on a successful match.

You might add a default, catch-all pattern at the end. The underscore is our first example of an actual pattern. It’s the wildcard pattern and means “match on anything”.

A pattern can be as follows:

  • A wildcard match (_).

  • A literal match, meaning equality, used for values such as 101 or RED.

  • A constructor match, meaning that a value would match if it could have been created using a specific constructor.

  • A deconstruction match, otherwise known as an extractor pattern.

  • A match based on a specific type, known as a type query pattern.

  • A pattern with alternatives (specified with |).

Patterns can also include a variable name, which on matching will be available to the expression on the right-hand side. It’s what’s referred to as a variable ID in the language specification.

There are some more which I’ve left off; if you’re interested see the Pattern Matching section of the Scala Language Specification.1

Literal Matches

A literal match is a match against any Scala literal. The following example uses a string literal and has similar semantics to a Java switch statement.

  val language = "French"
  value match {
      case "french" => println("Salut")
      case "French" => println("Bonjour")
      case "German" => println("Guten Tag")
      case _        => println("Hi")
  }

The value must exactly match the literal in the case. In the example, the result will be to print “Bonjour” and not “Salut” as the match value has a capital F. The match is based on equality (==).

Constructor Matches

Constructor patterns allow you to match a case against how an object was constructed. Let’s say we have a SuperHero class that looks like this:

case class                
  SuperHero(heroName: String, alterEgo: String, powers: List[String])

It’s a regular class with three constructor arguments, but the keyword case at the beginning designates it as a case class. For now, that just means that Scala will automatically supply a bunch of useful methods for us, like hashCode, equals, and toString.

Given the class and its fields, we can create a match expression like this:

1   object BasicConstructorPatternExample extends App {
2     val hero =
3       new SuperHero("Batman", "Bruce Wayne", List("Speed", "Agility"))
4
5     hero match {
6       case SuperHero(_, "Bruce Wayne", _) => println("I'm Batman!")
7       case SuperHero(_, _, _)             => println("???")
8     }
9   }

Using a constructor pattern, it will match for any hero whose alterEgo field matches the value “Bruce Wayne” and print “I’m Batman!”. For everyone else, it’ll print question marks.

The underscores are used as placeholders for the constructor arguments; you need three on the second case (line 7) because the constructor has three arguments. The underscore means you don’t care what their values are. Putting the value “Bruce Wayne” on line 6 means you do care and that the second argument to the constructor must match it.

With constructor patterns, the value must also match the type. Let’s say that SuperHero is a subtype of a Person, as shown in Figure 16-1.

A456960_1_En_16_Fig1_HTML.jpg
Figure 16-1 SuperHero is a subtype of Person

If the hero variable was actually an instance of Person and not a SuperHero, nothing would match. In the case of no match, you’d see a MatchError exception at runtime. To avoid the MatchError, you’d need to allow non-SuperHero types to match. To do that, you could just use a wildcard as a default.

  object BasicConstructorPatternExample extends App {
    val hero = new Person("Joe Ordinary")


    hero match {
      case SuperHero(_, "Bruce Wayne", _) => println("I'm Batman!")
      case SuperHero(_, _, _)             => println("???")
      case _                              => println("I'm a civilian")
    }
  }

Patterns can also bind a matched value to a variable. Instead of just matching against a literal (like “Bruce Wayne”) we can use a variable as a placeholder and access a matched value in the expression on the right-hand side. For example, we could ask the following question:

  • “What super-powers does an otherwise unknown person have, if they are a superhero with the alter ego Bruce Wayne?”

1   def superPowersFor(person: Person) = {
2     person match {
3       case SuperHero(_, "Bruce Wayne", powers) => powers
4       case _                                   => List()
5     }
6   }
7
8   println("Bruce has the following powers " + superPowersFor(person))

We’re still matching only on types of SuperHero with a literal match against their alter ego, but this time the underscore in the last position on line 3 is replaced with the variable powers. This means we can use the variable on the right-hand side. In this case, we just return it to answer the question.

Variable binding is one of pattern matching’s key strengths. In practice, it doesn’t make much sense to use a literal value like “Bruce Wayne” as it limits the application. Instead, you’re more likely to replace it with either a variable or wildcard pattern.

  object HeroConstructorPatternExample extends App {
    def superPowersFor(person: Person) = {
      person match {
        case SuperHero(_, _, powers) => powers
        case _                       => List()
      }
    }
  }

You’d then use values from the match object as input. To find out what powers Bruce Wayne has, you’d pass in a SuperHero instance for Bruce.

  val bruce =
    new SuperHero("Batman", "Bruce Wayne", List("Speed", "Agility"))
  println("Bruce has the following powers: " + superPowersFor(bruce))

The example is a little contrived as we’re using a match expression to return something that we already know. But as we’ve made the superPowersFor method more general purpose, we could also find out what powers any superhero or regular person has.

  val steve =
    new SuperHero("Capt America", "Steve Rogers", List("Tactics", "Speed"))
  val jayne = new Person("Jayne Doe")


  println("Steve has the following powers: " + superPowersFor(steve))
  println("Jayne has the following powers: " + superPowersFor(jayne))

Type Query

Using a constructor pattern , you can implicitly match against a type and access its fields. If you don’t care about the fields, you can use a type query to match against just the type.

For example, we could create a method nameFor to give us a person or superhero’s name, and call it with a list of people. We’d get back either their name, or if they’re a superhero, their alter ego.

 1   object HeroTypePatternExample extends App {
 2
 3     val batman =
 4       new SuperHero("Batman", "Bruce Wayne", List("Speed", "Agility"))
 5     val cap =
 6       new SuperHero("Capt America", "Steve Rogers", List("Tactics", "Speed"))
 7     val jayne = new Person("Jayne Doe")
 8
 9     def nameFor(person: Person) = {
10       person match {
11         case hero: SuperHero => hero.alterEgo
12         case person: Person => person.name
13       }
14     }
15
16     // What's a superhero's alter ego?
17     println("Batman's Alter ego is " + nameFor(batman))
18     println("Captain America's Alter ego is " + nameFor(cap))
19     println("Jayne's Alter ego is " + nameFor(jayne))
20   }

Rather than use a sequence of instanceOf checks followed by a cast, you can specify a variable and type. In the expression that follows the arrow, the variable can be used as an instance of that type. So, on line 11, hero is magically an instance of SuperHero and SuperHero specific methods (like alterEgo) are available without casting.

When you use pattern matching to deal with exceptions in a try and catch, it’s actually type queries that are being used.

  try {
    val url = new URL("http://baddotrobot.com")
    val reader = new BufferedReader(new InputStreamReader(url.openStream))
    var line = reader.readLine
    while (line != null) {
      line = reader.readLine
      println(line)
    }
  } catch {
    case _: MalformedURLException => println("Bad URL")
    case e: IOException => println("Problem reading data : " + e.getMessage)
  }

The underscore in the MalformedURLException match shows that you can use a wildcard with type queries if you’re not interested in using the value.

Deconstruction Matches and unapply

It’s common to implement the apply method as a factory-style creation method; a method taking arguments and giving back a new instance. You can think of the special case unapply method as the opposite of this. It takes an instance and extracts values from it; usually the values that were used to construct it.

apply (a, b) → object (a, b)

unapply (object (a, b))) → a, b

Because they extract values, objects that implement unapply are referred to as extractors.

  • Given an object, an extractor typically extracts the parameters that would have created that object.

So, if we want to use our Customer in a match expression, we’d add an unapply method to its companion object. Let’s start to build this up.

  class Customer(val name: String, val address: String)

  object Customer {
    def unapply(???) = ???
  }

An unapply method always takes an instance of the object you’d like to deconstruct, in our case a Customer.

  object Customer {
    def unapply(customer: Customer) = ???
  }

It should return either the extracted parts of the object or something to indicate it couldn’t be deconstructed. In Scala, rather than return a null to represent this, we return the option of a result. It’s the same idea as the Optional class in Java.

  object Customer {
    def unapply(customer: Customer): Option[???] = ???
  }

The last piece of the puzzle is to work out what can optionally be extracted from the object: the type to put in the Option parameter. If you wanted to be able to extract just the customer name, the return would be Option[String], but we want to be able to extract both the name and address (and therefore be able to match on both name and address in a match expression).

The answer is to use a tuple, the data structure we saw earlier. It’s a way of returning multiple pieces of data in a single type.

  object Customer {
    def unapply(customer: Customer): Option[(String, String)] = {
      Some((customer.name, customer.address))
    }
  }

We can now use a pattern match with our customer.

  val customer = new Customer("Bob", "1 Church street")
  customer match {
    case Customer(name, address) => println(name + " " + address)
  }

You’ll notice that this looks like our constructor pattern example. That’s because it’s essentially the same thing; we used a case class before, which added an unapply method for us. This time we created it ourselves. It’s both an extractor and, because there’s a symmetry with the constructor, a constructor pattern.

More specifically, the list of values to extract in a pattern must match those in a class’s primary constructor to be called a constructor pattern. See the language spec2 for details.

Why Write Your Own Extractors?

Why would you implement your own extractor method (unapply) when case classes already have one? It might be simply because you can’t or don’t want to use a case class or you may not want the match behavior of a case class; you might want custom extraction behavior (for example, returning Boolean from unapply to indicate a match with no extraction).

It might also be the case that you can’t modify a class but you’d like to be able to extract parts from it. You can write extractors for anything. For example, you can’t modify the String class but you still might want to extract things from it, like parts of an email address or a URL.

For example, the stand-alone object in the following example extracts the protocol and host from a string when it’s a valid URL. It has no relationship with the String class but still allows us to write a match expression and “deconstruct” a string into a protocol and host.

  object UrlExtractor {
    def unapply(string: String): Option[(String, String)] = {
      try {
        val url = new URL(string)
        Some((url.getProtocol, url.getHost))
      } catch {
        case _: MalformedURLException => None
      }
    }
  }


  val url = "http://baddotrobot.com" match {
    case UrlExtractor(protocol, host) => println(protocol + " " + host)
  }

This decoupling between patterns and the data types they work against is called representation independence (see Section 24.6 of Programming in Scala ).3

Guard Conditions

You can complement the patterns we’ve seen with if conditions .

customer.yearsACustomer = 3
  val discount = customer match {
    case YearsACustomer(years) if years >= 5 => Discount(0.50)
    case YearsACustomer(years) if years >= 2 => Discount(0.20)
    case YearsACustomer(years) if years >= 1 => Discount(0.10)
    case _ if blackFriday(today)             => Discount(0.10)
    case _                                   => Discount(0)
  }

The condition following the pattern is called a guard. You can reference a variable if you like, so we can say for customers of over five years, a 50% discount applies; two years, 20%, and so on. If a variable isn’t required, that’s fine too. For example, we’ve got a case that says if no previous discount applies and today is Black Friday, give a discount of 10%.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.59.173.242