Pattern matching is an important part of Scala and can be thought of as a generalized version of Java’s switch-Case statement. In real Scala applications, you can write highly abstract logic using a combination of pattern matching and recursion. In addition to pattern matching itself, sample classes (also known as template classes) and extractors are also covered here.

1. Basic usage

Scala’s pattern matches are declared using the match keyword, each branch uses the case keyword, and then a block of statements is joined with the => symbol. Unlike the Java switch statement, there is no crossover between cases. When a match is performed, the program attempts a match from the first branch and only executes the first successful match block.

In other words, the order in which a case is declared affects the results of the program. Therefore, the more “precise and specific” the case should be, the higher the priority, or “the higher the case”. Here is a simple pattern matching example:

val a = 10
val b = 20
var operator = '+'
var result = 0

operator match {
    //case [condition] => {Block}
    case '+' => result = a + b
    case '-' => result = a - b
    case '*' => result = a * b
    case '/' ifb ! =0 => result = a / b
    case _ =>
        print("invalid operator.")
        result = - 1
}

println(s"result = $result")
Copy the code

This code is the most basic use of pattern matching (similar to the switch statement). When matching literals, all the program has to do is compare the values of the operator to those literals.

Note that if all case branches do not meet the criteria, the application will throw an ErrorMatch exception, because Scala considers this a “bug” caused by the programmer’s poor design.

Sometimes to avoid exception throws, we often end pattern matches with case _ (as in the above code block), which is a wildmatch pattern. The _ symbol here indicates that the operator variable is ignored and the corresponding code block is run directly. Obviously, this match should have the lowest priority, and we only use it as a “not valid in any other case” backup, analogous to the default route entry with the lowest priority in the router forwarding table.

1.1 Insert conditional guard

In addition to case matching, conditional guards can be used to insert additional judgments, written like case… if … = >… . For example, in the following code block, given a value value, we use conditional guards to realize the interval judgment of value:

val value = 12

value match{
    case _ if value>0 && value<50 => println("The value is in the 0-50 range.")
    case _ if value>=50 && value<100 => println("The value is in the 50-100 range.")
    case _ if value>=100 => println("The value is 100 and above.")}Copy the code

The program will run the corresponding code block only if both case and if conditions are met. In addition, each function block can be indented instead of curly braces, keeping the code readable and cleaner.

value match {
    case 100 =>
        println("Blocks of code for each branch can be left out of parentheses.")
        println("But you need blank lines.")
    case _ => println("If I were to write it on the same line"); println("Separate statements with a semicolon.")}Copy the code

1.2 Mode Matching has a return value

Every block of Scala statements has a return value (even Unit), and pattern matching is no exception:

val value = 1000

val result: Int = value match {
  // The branch satisfies the condition, so the pattern match returns 1.
  case 1000 => println("value = 1000.") 1
  case_ = >0
}

if (result == 1) println("Pattern match successful")
Copy the code

The exact return value depends on which branch the program ultimately chooses. Also, all case branches should return a value of the same type, or all return Unit.

2. Type matching

The case keyword can be followed by a temporary immutable variable that applies only to the current branch, such as a and B in the following code blocks:

val result =  value match {
    case a => println(a)
    case b => println(b)
}
Copy the code

When the first branch is checked, the value of value is passed to A; When the second branch is checked, value is passed to B. Of course, pure assignment is not meaningful, we need to use the assignment operation and condition judgment together, here is the use of type matching.

Suppose now four identity: Student, the Teacher, Doctor, Anonymity, they all inherit the Person class. The task now is to use pattern matching to check the actual type of a transition object person:

val person: Person = new Student
val value = person match {
    // These p variables do not conflict because they only exist in their scope.
    case p: Student => println("He is a student.")
    1
    case p: Teacher => println("He's a teacher.")
    2
    case p: Doctor => println("He is a doctor.")
    3
    case _ => println("He was a mystery client, presumably Anonymity type.")
    4
}
print(S "pattern matching result:$value")
Copy the code

Within each branch, the program does two steps: first it assigns value to p, and then it checks the type of P. Note that the variable P for each branch is independent and there is no relationship between them. P: Student stands for “if p is of Student type”. We’ve done this before with isInstanceOf[] and asInstanceOf[], and now we just need type matching to solve the problem.

Scala rejects pattern matches that are obviously unreasonable and prompts syntax errors. For example, the following pattern matches that person can be a string, but clearly person can only be Student or one of its derived subclasses.

val person: Person = new Student
val value = person match {
  case p: Student => println("He is a student.")
    1
  case p: String => println("This is an invalid match.")
    2
}
Copy the code

Alternatively, if you type only person and don’t want to use the assigned value, you can hide the variable by using _.

val person: Person = new Student
val value = person match {
	// Only the type of value is evaluated.
    case_ :Student => println("He is a student.")
    1
    case_ :Teacher => println("He's a teacher.")
    2
    case_ :Doctor => println("He is a doctor.")
    3
    case _ => println("He was a mystery client, presumably Anonymity type.")
    4
}
print(S "pattern matching result:$value")
Copy the code

2.1 Type erasure affects judgment

Java’s generic erasure mechanism also affects Scala. This rule also applies to all Scala collections that use generics: Set, List, Map, ArrayBuffer… But not Array (for reasons explained below). Observe that no matter what generic Map[K,V] is passed in, the pattern match is always returned in the first case because the exact type of Map cannot be determined.

val stringToInt = Map("1" -> 2)
val intToString = Map(2 -> "1")

def checkMap(any: Any) :Unit = {
  any match {
    // Due to type erasing, this pattern match is degraded from "determine what type of Map" to" determine whether Map class"
    case_ :Map[String.String] => println("Map[String,String]")
    case_ :Map[Int.Double] => println("Map[Int,Double]")
    case_ :Map[Double.Int] => println("Map[Double,Int]")
    case _ => println("Map[?,?] ")
  }
}

checkMap(stringToInt)
checkMap(intToString)
Copy the code

This problem needs to be solved by relying on more powerful type systems. We will show how to use TypeTag to extract both “classes” and “types” in the reflection section at runtime.

The exception is the Array type. Pattern matching directly identifies the Array[T] type because Scala translates all the Array[T] types into the corresponding T[] type in Java.

val ints: Array[Int] = Array[Int] (1.2.3)
val strings: Array[String] = Array[String] ("1"."2"."3")

def checkMap(any: Any) :Unit = {
  any match {
    case_ :Array[Int] => println("Array[Int]")
    case_ :Array[String] => println("Array[String]")
  }
}

checkMap(ints)
checkMap(strings)

// Scala Array[Int] => Java Int []
println(ints.getClass)

// Scala Array[String] =>Java String[]
println(strings.getClass)
Copy the code

3. Object matching

In addition to matching types, match statements can also be accurate to match objects and extract internal data. We’ll talk about matching arrays, lists, tuples first, and then we’ll talk about matching other objects.

3.1 Matching Array

Using match statements to match Array types (such as Array and ArrayBuffer) is much more efficient, and the increase in code readability is huge. The extractor stuff is actually covered here, but that doesn’t stop us from “getting” the meaning of pattern matching from the following code block: case Array(1,2,3) indicates whether ints is an Array of Array(1,2,3) style. There is, of course, an implicit precondition: InTS is an Array of type in the first place.

In addition, symbols such as _ and _* can be combined to specify the matching mode. The _ symbol is already familiar, and the _* means **” any element after “**. Note that the _* symbol can only be placed at the end.

val ints: Array[Int] = Array[Int] (1.2)

ints match {
  case Array(1.2.3) =>println("This array has 1,2,3.")
  case Array(_,_) => println(S "This array contains two elements.")
  case Array(0,_*) => println(S "This array starts with 0.")
  case Array(1.2.3,_*) => println(S "this array starts with 1,2,3.")
  case _ => println("None of them match the rules.")}Copy the code

Imagine how difficult it would be to write in Java to say “an array has two elements”, “begins with 1,2,3” or even “the middle element is 3”. In addition, using pattern matching we can easily implement element exchange:

ints match {
  case Array(0) = >Array(1.2.3)
  case Array(x,y) => Array(y,x)
  case Array(x,y,z) => Array(x,z,y)
  case _ if ints.length>5= >Array(ints(0),ints(1),ints(2))
  case_ = >Array(0)}Copy the code

As an aside, there’s another use for the _* symbol. Let’s have a variable parameter function:

def ints(ints: Int*) : Unit = println(ints)
Copy the code

In some cases, we may want to pass all elements of a Seq sequence into INTS as indeterminate arguments. However, the following would not pass:

val seq = Seq(1.2.3)
ints(seq)
Copy the code

The compiler thinks the argument list expects multiple ints, but we pass in the entire sequence of Seq[Int]. To clear up this confusion, we use the _* symbol to indicate that the elements in the sequence are taken out and placed in the argument list.

ints(seq:_*)
Copy the code

3.2 Matching List

For List matching, you can also use :: or ::: (These two symbols, described in the List section of collections, are actually extractors, but that doesn’t stop us from using them in an intuitive way) to combine highly abstract List matching patterns.

val ints = List[Int] (1.2.3.4.5.6)

ints match {
    case List(1.2) => println("This array is (1,2).")
    case List(1, _*) => println("This array starts with a 1.")
    
    // Match the list. You can also use the :: hyphen to indicate the array to be matched
    case 1: : _ : :Nil => println("This array starts with a 1, followed by one element.")
    case 1 :: _ => println("This array starts with a 1, but it doesn't care how many elements follow it.")
    
    case_ : :2: :Nil => println("This array has two elements, the second of which is 2.")
    case _ :: tail => println("You get the rest of 2,3,4,5,6.")
    
    case 2 :: left =>
        println("This array starts with a 2, and the remaining elements are :")
        for (i <- left) println(i)
    case _ => println("None of the above conditions are met.")}Copy the code

3.3 Matching Tuples

Examples of matching tuples are given below. Note that tuples are not the same as lists or arrays, and you cannot use the _* symbol for “indeterminate number of elements” because each tuple should be of an explicit TupleX type.

tuple2 match {
  case (2."tuple3") => println("The tuple is :(2,\"tuple3\")")
  case (2,_) => println("This binary begins with a 2.")
  case (x,y) => (y,x)  // Transpose the elements of the tuple.
  case _=> println("None of the matching conditions are met.")}Copy the code

4. The extractor

Now we have a new requirement to determine if the passed parameter is of type Student by pattern matching; At the same time, judge whether its grade is greater than 60; Print out the name of the student as well. The following is a simple definition of the given Student class:

object Student {
  def apply(name : String,grade : Int) :Student = new Student(name,grade)
}
class Student(var name : String,var grade : Int) 
Copy the code

The first solution can express this logic by combining type matching and conditional guarding:

def isStudent(i: Any) :Boolean = {
  i match {
    case i: Student if i.grade >= 60 =>
      println(s"this student(${i.name})'s grade is ok."); true
    case i: Student if i.grade < 60 =>
      println(s"this student(${i.name})'s grade is lower than 60."); true
    case _ =>
      println("not match"); false}}Copy the code

This works, but it’s a bit verbose compared to the previous matching array, matching list, etc. Why can’t we just use case Student(grade,name) to extract the attributes inside the object as we did before? Or, why can arrays, lists, and tuples extract elements directly as Array(x,y,z), List(x,y,z), or (x,y,z)?

These classes themselves implement the function of an extractor, thus allowing direct extraction of internal elements during pattern matching. For custom classes, we also need to design them as extractors.

4.1 by an unapply method

Extractors refer to singleton objects (or associated objects) that have unapply methods. By name, the unapply method is relative to the apply factory method used to construct objects.

Again, Student(name,grade) is an “injection” for the Apply method: Name and grade are provided, and the method returns the instance. For the unapply method, this is an “extract” operation: in pattern matching, if it is determined to be of the Student type, the corresponding attribute is extracted into the name and grade variables.

Apply and unapply are dual relationships, or reciprocal processes. For extractors, you may not define apply methods, but you must define unapply methods. But to satisfy this duality, extractors generally implement the Apply factory method as well. The following call:

Student.unapply(Student.apply(name,grade))
Copy the code

One should return: Some((name,grade)), Some is the wrapper class that belongs to Option. If two or more attributes are extracted, these elements are also wrapped in tuples. But if only one attribute is extracted, the single element is wrapped directly into Some. In the case of a pattern mismatch, the unapply method should return None.

As in the previous example of swapping elements to match an INTS array, we can imagine pattern matching actually doing this:

/* ints match{ case Array(x,y) => Array(y,x) ... } * /

Array.unapply(int) match{
	case Some((x,y)) => Array.apply(y,x)
    ...
}
Copy the code

If the pattern match is successful and the int is unapplied to Array, it will return the corresponding property correctly.

Returning to the case, we can now design such unapply methods for the Student class to be called when the pattern matches. Note that options are wrapped in tuples, and you should find out what attributes the elements in each position represent before returning the result.

def unapply(arg: Student) :Option[(String.Int)] = Some(arg.name,arg.grade)
Copy the code

Now we can get back to extracting properties directly from Student objects in a concise form:

def isStudent(i: Any) :Boolean = {
  i match {
   // Extract values will be assigned to name and grade.
    case Student(name,grade) if grade >= 60 =>
      println(s"this student($name)'s grade is ok."); true
    case Student(name,grade) if grade < 60 =>
      println(s"this student($name)'s grade is lower than 60."); true
    case _ =>
      println("not match"); false}}Copy the code

Unapply also does not extract any elements, at which point its return value degrades from Option to Boolean. The extractor does not provide any extraction values for pattern matching, but merely serves as a judgment. For example, we directly encapsulate the logic for judging grades in the unapply method:

def unapply(arg: Student) :Boolean = if(arg.grade >= 60) true else false
Copy the code

External pattern matching no longer requires any extraction values and condition guards. Also, because the unapply method does not provide an extract value, it is followed by an empty parenthesis. The empty parentheses are required, otherwise it would be a match between the variable I and the singleton Student.

def isStudent(i: Any) :Boolean = {
  i match {
    case Student() => println("this student's grade is ok.");true
    case _ =>
      println("not match"); false}}Copy the code

But for this case, we should not hide the judgment logic inside Student’s unapply method. The caller to the code who does not know the inner details of Unapply has no idea what case Student() => really means. We’d better create another extractor and name it properly to indicate what this extractor can do:

object Student {
  
  object passedExam{
    def unapply(arg: Student) :Boolean = arg.grade>=60
  }
  
  def apply(name: String, grade: Int) :Student = new Student(name, grade)
  def unapply(arg: Student) :Boolean = if(arg.grade >= 60) true else false
}
Copy the code

This makes pattern matching much more readable than it was before.

i match {
    case Student.passedExam() => println(s"this student's grade is ok.");true
    case _ => println("not match"); false
}
Copy the code

Even so, I don’t recommend it. Unapply methods are only responsible for “extracting” attributes and should not be over-engineered within the method itself. We should use conditional guards to place additional judgment logic in a “more visible location” to improve the readability of the code.

4.2 unapplySeq method

When the unpply method extracts a return value with more than one element, we should instead choose the unapplySeq method for variable length parameter matching. For example, the following extractor can parse a String as a symbol and extract the corresponding word using unapplySeq:

object Words{
  def unapplySeq(sentence : String) : Option[Seq[String]] = {
    if(sentence.contains(",")) {Some(sentence.split(","))}else None}}Copy the code

Since we don’t know in advance how many words can be split, we need Option[Seq[String]] as the return value here.

"hello,world,java and scala" match {
  case Words(a,"world",b) => println(s"$a.$b")}Copy the code

4.3 @ symbol is associated with multiple extraction results

For the unapplySeq method, if you want to associate multiple extracted values to a sequence, you can use the @ symbol, as in:

"hello,python,java and scala" match {
  case Words(a, "world", b) => println(s"$a.$b")
  // All words except the first are associated with the tail variable. Tail in this case belongs to Seq[String].
  case Words(_, tail @_*) => tail foreach(println(_))
}
Copy the code

In this example, the subsequent extract value represented by _* is bound to the tail variable by the @ symbol.

5. The sample class

Only the extractor can be used for object matching, and if you want to extract object attributes quickly, you always need to manually complete the unapply method. Of course, you also need to declare the associated object…… before doing this In most cases, this is repetitive work, just as we always need to complement the GET and set methods for Java beans.

class CaseClass(val x : Int , val y : Int)
object CaseClass{
  def apply(x : Int,y : Int) : CaseClass = new CaseClass(x,y)
  def unapply(arg: CaseClass) :Option[(Int.Int)] = Some((arg.x,arg.y))
}
Copy the code

Scala takes this into account and provides us with an enhanced sample class to simplify the code by adding an extra case identifier to the front of the class declaration:

case class CaseClass(x : Int,y : Int)
Copy the code

This line of code is equivalent to the “big block declaration” above. First, the compiler automatically constructs companion objects with unapply and apply methods for us, and second, all parameters in the main constructor are automatically treated as immutable members of type Val. This short declaration makes the sample class declaration and object matching visually consistent:

case CaseClass(x,y) => ...
Copy the code

In addition, the compiler automatically implements the toString, hashCode, and equal methods, as well as the copy method for deep copy. Here’s an example:

val caseClass  : CaseClass = CaseClass(1.2)
// It is a reference to another object, but all the values are the same as caseClass.
val caseClass_temp : CaseClass = caseClass.copy()
Copy the code

If you want to overwrite some values before copying, you can also specify it in the parameter list of the copy method.

val caseClass  : CaseClass = CaseClass(1.2)
/ / caseClass (1, 10)
val caseClass_temp : CaseClass = caseClass.copy(y = 10)
Copy the code

Patterns, recursion, and divide-and-conquer philosophy

Here is a direct example to illustrate. How do you use pattern matching to rejoin two ordered sequences, LIST1 and List2, into a longer ordered list?

// Given two ordered arrays
val list1 = List(1.3.6.9)
val list2 = List(4.5.7.8)

// Given the merge method to implement
/ / note:????? Is a thrown at run time scala. NotImplementedError method, when we write a program, you can use it to do a temporary placeholder symbols.
def merge(xs: List[Int], ys: List[Int) :List[Int] =???// the desired result is 1,3,4,5,6,7,8,9
println(merge(list1, list2).mkString(","))
Copy the code

Before implementing the merge method, I make a few symbolic declarations here. For the first List[Int] parameter xs, assume that its first element is x1 and the remaining elements are marked with x1_~>. Similarly, the parameters ys are divided into y1 and y1_~>.

Since the condition is given that both lists are already sorted, you only need to compare x1 and y1 each time without thinking about the rest. If x1 < y1, you should place x1 first and repeat the process again from the x1_~> and ys lists, extracting the next smaller element (the same is true for x1 > y1).

Combining pattern matching with the :: notation, we can easily write elegant recursive procedures. The special case where xs or ys is Nil is added:

def merge(xs: List[Int], ys: List[Int) :List[Int] = (xs, ys) match {
    case (Nil, _) => ys
    case (_, Nil) => xs
    case (x1 :: x1_~>, y1 :: y1_~>) =>
    if (x1 < y1) x1 :: merge(x1_~>, ys)
    else y1 :: merge(y1_~>, xs)
}
Copy the code

In fact, we just need to take this merge method a step further and create a Two-way merge sort implementation in Scala. Merge sort splits a long list in half into two subsequences and merges them using the merge method. In order to ensure that the two sub-sequences are ordered, it is necessary to separate and merge the two sub-sequences again, so as to recurse. The above logic is expressed in Scala as follows:

def mergeSort(xs: List[Int) :List[Int] = {
    def merge(xs: List[Int], ys: List[Int) :List[Int] = (xs, ys) match {
        case (Nil, _) => ys
        case (_, Nil) => xs
        case (x1 :: x1_~>, y1 :: y1_~>) =>
        if (x1 < y1) x1 :: merge(x1_~>, ys)
        else y1 :: merge(y1_~>, xs)
    }

    val n : Int =  xs.length / 2
    if(n == 0) xs    // Xs is already a nondivisible atom.
    else {
        val (left, right) = xs splitAt n  // The splitAt method divides the list elements from index numbers (0 to n-1) into the left half and the rest into the right half.
        merge(mergeSort(left), mergeSort(right))
    }
}
Copy the code

In this example, we split, merged, and recursively handled a divide-and-conquer problem perfectly using just pattern matching with the :: symbol. In addition, we can elegantly design a functional data structure through pattern matching + recursion.

7. Patterns everywhere

In addition to match statements, Scala applies pattern-matching styles elsewhere, such as in partial functions, variable assignments, and even for loops.

7.1 Mode matching style assignment

Sometimes we are faced with this way of initializing a variable:

val x  =1 
val y = 2
val z = 3 
Copy the code

You can use “pattern matching” to write batch assignments to multiple variables to simplify code, such as:

// This is equivalent to:
//val x = 1
//val y = 2
//val z = 3
val (x, y, z) = (1.2.3)
Copy the code

Pattern matching style assignments can also be extended to Array or List cases.

// a = 1 
val Array(a, _*) = Array(1.2.3.4)

val list = List(1.2.3.4.5)
// head = 1; b = 2; c= 4; tail = 5;
val head :: b  :: 3 :: c :: tail  = list
Copy the code

Note, however, that it is also possible to raise a MatchError exception when an assignment is not matched correctly.

val list = List(1.2.3.4.5)
val head :: b  :: 5 :: c :: tail  = list
// Exception in thread "main" scala.MatchError: List(1, 2, 3, 4, 5) (of class scala.collection.immutable.$colon$colon)
Copy the code

7.2 Patterns in the For Loop

Pattern matching can also be used in a for loop, such as the following example to collect all values = “JVM” keys from the MAPS map and print it:

val maps = Map("scala" -> "jvm"."java" -> "jvm"."c++" -> "c"."c#" -> "c")

// Pattern matching in the for loop.
val strings = for ((k, "jvm") <- maps) yield k

println(strings.mkString(","))
Copy the code