1. Overview

In this quick article, we’re going to introduce extractor objects and use cases for them in Scala pattern matching.

In Scala, every object that has an unapply() method is called an extractor object. The unapply() method is the basis of pattern matching in Scala because this method helps us to extract data values compacted in objects.

2. apply() and Factory Methods

Since the unapply() method is the opposite of the apply() method, before introducing extractor objects, we need to know about the apply() method.

Consider a User class with default constructor:

class User(val name: String, val age: Int)

Whenever we want to create an instance of User, we need to use the new keyword to invoke the constructor:

val user = new User("John", 25)

We can use the apply() method as a factory method in the User’s companion object:

object User {
  def apply(name: String, age: Int) = new User(name, age)

Now, by calling the apply() method, we can instantiate our User class:

val user = User.apply("John", 25)

For neater code, we use the syntactic sugar version of apply() by just calling the class name without the apply() method:

val user = User("John", 25)

Also, we can write multiple apply() methods for our User class:

object User {
  def apply(name: String, age: Int) = new User(name, age)
  def apply(name: String) = new User(name, 0)

So, just by calling User(“Jack”), we can instantiate User with an age of zero.

3. unapply() and Extractor Objects

The unapply() method is just the opposite of the apply() method. This method extracts all object values and lists them as a result. The best practice is to put the unapply() method in the companion object of our class:

object User {
  def unapply(u: User): Option[(String, Int)] = Some(u.name, u.age)

Now we can call unapply() explicitly:

scala> User.unapply(user)
res8: Option[(String, Int)] = Some((jack,30))

This is how pattern matching in Scala extracts object values. Without the unapply() method, we can’t do pattern matching:

user match {
  case User(_, age) if age < 18 => 
    println("You are not allowed to get a driver license.")
  case User(_, age) if age >= 18 =>
    println("You are allowed to get a driver's license.")

Keep in mind that we can write a customized unapply() method to extract more details from our objects. Think of an extractor object that extracts day, month, and year from a date-time string like “2019-05-28”.

4. unapplySeq() and Deconstructing Sequences

The unapply() method is useful when we’re going to extract various single-value items, such as the name and age from a User. On the other hand, if we want to deconstruct an instance to a collection of values, we should use the unapplySeq() method.

For example, let’s suppose we’re going to deconstruct an HttpRequest instance to its Headers:

case class Header(key: String, value: String)
class HttpRequest(val method: String, val uri: String, val headers: List[Header])

If we declare a unapplySeq() method in the companion object:

object HttpRequest {
  def unapplySeq(request: HttpRequest): Option[List[Header]] = Some(request.headers)

Then we can deconstruct the request into its constituent headers and apply pattern matching to that collection of headers:

val headers = Header("Content-Type", "application/json") ::
  Header("Authorization", "token") :: Header("Content-Language", "fa_IR") :: Nil
val request = new HttpRequest("GET", "localhost", headers)
request match {
  case HttpRequest(h1) => println(s"The only header is $h1")
  case HttpRequest(h1, h2) => println(s"We have two headers: $h1 and $h2")
  case HttpRequest(all @ _*) => print(s"All headers are as following: $all")

The first and second cases will be matched if the request contains one or two headers, respectively. In the latter case, we bind a repeated number of headers (_*” part) to the variable all. Obviously, the latter case will match the given request, so this program prints:

All headers are as following: List(Header(Content-Type,application/json), 
  Header(Authorization,token), Header(Content-Language,fa_IR))

If we replace the unapplySeq() method with unapply(), the same pattern will fail with the message:

too many patterns for object HttpRequest offering List[Header]: expected 1, found 2

Put simply, when using unapply(), the whole List[Header] will be considered as a single value, whereas unapplySeq() allows us to pattern match against each member of the sequence.

5. Conclusion

While apply() is a way of writing factory methods for our data types and classes, unapply() is good for deconstructing the class into values. What makes apply() and unapply() special is creating and deconstructing objects silently.

We use our apply() method just by calling User(“Jack”, 30) instead of User.apply(“Jack”, 30). Also, by pattern matching on the user object, the unapply() method is called silently.

As usual, all the code implementations are available over on GitHub.