3.6 Pattern matching and case classes

One of the core tenets of object-oriented programming is encapsulation: objects have private state, and access to that private state is controlled by methods on the object. Encapsulation encourages scalable design in that an object's implementation can evolve without impacting client code. While encapsulation is simple to achieve with pure value-objects, where getter and setter methods may suffice, more complex objects can contain state for which there are no readily available accessor methods.

Pattern matching, a functional language technique that dates back to the 1970s, helps in those situations. Similar to a switch statement, pattern matching allows you to match an object's state against a pattern. That pattern closely mirrors the code used to create the object.

For instance, consider a ConfirmationMessage, with a paymentStatus and a shippingStatus field. Pattern matching allows you to use the object's state in switch-like manner:

  val status = 
    message match {
      case ConfirmationMessage(PaidShipped) =>
        Status("Order on its way") 
      case ConfirmationMessage(PaidPending) =>
        Status("Wrapping the order")
      case ConfirmationMessage(PaidReturned) =>
        Status("That's too bad!")
      case ConfirmationMessage(Declined, _) =>
        Status("Wrong credit card")
      case _ => Status("Unknown status")
    }

Although ConfirmationMessage(Paid, Shipped) looks like the construction of a ConfirmationMessage instance, it instead denotes a pattern against which message is matched. Scala's pattern matching picks apart the message object and finds out if message is a ConfirmationMessage type, and if so, whether it's paymentStatus and shippingStatus values are Paid and Shipped, respectively.

The first pattern matching message causes the expression on the right of => to evaluate; subsequent patterns are not matched. If the expression to the right returns a value, as in this example, the value of that expression is returned from the match.

As the example illustrates, you can use wildcards in several places: the underscore in ConfirmationMessage(Declined, _) means that we don't care about the shipping status value. And, in the last line of the match block, a wildcard ensures that an unknown message status is matched.

The actor API uses pattern matching extensively in incoming message processing. An actor's message typically carries one or more value objects. For instance, a ConfirmationMessage includes references to values paymentStatus and shippingStatus. ConfirmationMessage's purpose, then, is to bundle those constituent values.

Scala's case classes provide a convenient way to define classes whose constructor wraps other data elements that are part of the class:

  case class ConfirmationMessage(
    paymentStatus: String, 
    shippingStatus: String
  )

All you need to do is preface the class with case, and list the class's constituent objects in the constructors. The Scala compiler adds some syntactic sugar to such classes, such as a proper implementation of equals and hashCode, and makes it possible for you to use case classes in pattern matching. You would almost always want to define your actor messages as Scala case classes.

Note that in the current implementation of Scala, match is a language construct, not a method, even though match appears as a control structure, similar to the control structures we defined earlier with methods. Indeed, in earlier versions of Scala, match was implemented as a method, but various implementation issues led to it now being defined as a special keyword. Nevertheless, for all practical purposes, you can think of match as a method consuming a list of pattern matching cases as its argument.

In the above example, the last pattern matching case, an underscore (_), is a wildcard that matches anything. But what if you left out that wild-card pattern? With the above example, the Scala compiler would complain and, if you ever passed into it a ConfirmationMessage that does not match one of your cases, match would result in a runtime exception.

Without a wild-card case pattern, the pattern matching cases match only a subset of possible argument values to match. Since a Scala function is an object of a specific type (a function type), you can create a function subclass that defines the function for only a specific range of the function's arguments. For instance, without the last wild-card pattern, the above definition of match defines the function for only four argument values (actually, the wildcard in the third case may match additional values, too). Scala's pattern matching is implemented in terms of such partial functions; and you will see the PartialFunction class as an argument type in the actor API.

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

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