In Scala, patterns can be defined independently of case classes. To this end, a method named unapply
is defined to yield a so-called extractor.
For instance, the following code defines an extractor object Twice
.
object Twice {
def apply(x: Int): Int = x * 2
def unapply(z: Int): Option[Int] = if (z % 2 == 0) Some(z / 2) else None
}
object TwiceTest extends Application {
val x = Twice(21)
x match { case Twice(n) => Console.println(n) } // prints 21
}
There are two syntactic conventions at work here:
case Twice(n)
will cause an invocation of Twice.unapply
, which is used to match even number; the return value of the unapply
signals whether the argument has matched or not, and any sub-values that can be used for further matching. Here, the sub-value is z/2
apply
method is not necessary for pattern matching. It is only used to mimick a constructor. val x = Twice(21)
expands to val x = Twice.apply(21)
.The code in the preceding example would be expanded as follows:
object TwiceTest extends Application {
val x = Twice.apply(21)
Twice.unapply(x) match { case Some(n) => Console.println(n) } // prints 21
}
The return type of an unapply
should be chosen as follows:
Boolean
. For instance case even()
T
, return a Option[T]
T1,...,Tn
, group them in an optional tuple Option[(T1,...,Tn)]
.Sometimes, the number of sub-values isn't fixed and we would like to return a sequence. For this reason, you can also define patterns through unapplySeq
. The last sub-value type Tn
has to be Seq[S]
. This mechanism is used for instance in pattern case List(x1, ..., xn)
.
When you create a case class, it automatically can be used with pattern matching since it has an extractor:
case class Employee(firstName: String, lastName: String)
val rob = new Employee("Robin", "Williams")
val result = rob match {
case Employee("Robin", _) => "Where's Batman?"
case _ => "No Batman Joke For You"
}
result should be(res0)
What's an extractor? In Scala it's a method in any object
called unapply
, and that method is used to disassemble the object given by returning a tuple wrapped in an option. Extractors can be used to assign values:
class Car(val make: String, val model: String, val year: Short, val topSpeed: Short)
object ChopShop {
def unapply(x: Car) = Some((x.make, x.model, x.year, x.topSpeed))
}
val ChopShop(a, b, c, d) = new Car("Chevy", "Camaro", 1978, 120)
a should be(res0)
b should be(res1)
c should be(res2)
d should be(res3)
An extractor can also be used in pattern matching:
class Car(val make: String, val model: String, val year: Short, val topSpeed: Short)
object ChopShop {
def unapply(x: Car) = Some((x.make, x.model, x.year, x.topSpeed))
}
val x = new Car("Chevy", "Camaro", 1978, 120) match {
case ChopShop(s, t, u, v) => (s, t)
case _ => ("Ford", "Edsel")
}
x._1 should be(res0)
x._2 should be(res1)
Since we aren't really using u
and v
in the previous pattern matching, they can be replaced with _
:
class Car(val make: String, val model: String, val year: Short, val topSpeed: Short)
object ChopShop {
def unapply(x: Car) = Some((x.make, x.model, x.year, x.topSpeed))
}
val x = new Car("Chevy", "Camaro", 1978, 120) match {
case ChopShop(s, t, _, _) => (s, t)
case _ => ("Ford", "Edsel")
}
x._1 should be(res0)
x._2 should be(res1)
As long as the method signatures aren't the same, you can have as many unapply methods as you want:
class Car(val make: String, val model: String, val year: Short, val topSpeed: Short)
class Employee(val firstName: String, val middleName: Option[String], val lastName: String)
object Tokenizer {
def unapply(x: Car) = Some((x.make, x.model, x.year, x.topSpeed))
def unapply(x: Employee) = Some((x.firstName, x.lastName))
}
val result = new Employee("Kurt", None, "Vonnegut") match {
case Tokenizer(c, d) => "c: %s, d: %s".format(c, d)
case _ => "Not found"
}
result should be(res0)
An extractor can be any stable object, including instantiated classes with an unapply method:
class Car(val make: String, val model: String, val year: Short, val topSpeed: Short) {
def unapply(x: Car) = Some((x.make, x.model))
}
val camaro = new Car("Chevy", "Camaro", 1978, 122)
val result = camaro match {
case camaro(make, model) => "make: %s, model: %s".format(make, model)
case _ => "unknown"
}
result should be(res0)
A custom extractor is typically created in the companion object of the class. In this exercise, we use it as an assignment:
class Employee(
val firstName: String,
val middleName: Option[String],
val lastName: String)
object Employee {
//factory methods, extractors, apply
//Extractor: Create tokens that represent your object
def unapply(x: Employee) =
Some((x.lastName, x.middleName, x.firstName))
}
val singri = new Employee("Singri", None, "Keerthi")
val Employee(a, b, c) = singri
a should be(res0)
b should be(res1)
c should be(res2)
In this exercise we use unapply
for pattern matching employee objects:
class Employee(
val firstName: String,
val middleName: Option[String],
val lastName: String)
object Employee {
//factory methods, extractors, apply
//Extractor: Create tokens that represent your object
def unapply(x: Employee) =
Some((x.lastName, x.middleName, x.firstName))
}
val singri = new Employee("Singri", None, "Keerthi")
val result = singri match {
case Employee("Singri", None, x) =>
"Yay, Singri %s! with no middle name!".format(x)
case Employee("Singri", Some(x), _) =>
"Yay, Singri with a middle name of %s".format(x)
case _ => "I don't care, going on break"
}
result should be(res0)