CHAPTER 8
Pattern matching is similar to Java’s switch/case mechanism. But, as we will see in Scala, pattern matching is more interesting and flexible than switch/case. Code Listing 81 shows a basic example of pattern matching.
Code Listing 81: Simple Matching Example 1
object MainObject {
// This function is an example of pattern matching: def matchFruit(index: Int): String = index match { case 1 => "Apple" case 2 => "Banana" case 3 => "Kumquat" case _ => "Unknown" }
def main(args: Array[String]): Unit = {
// 2 and 3 match banana and cumquat println("2's Case: " + matchFruit(2)) println("3's Case: " + matchFruit(3))
// Anything not mapped in the match/case matches _ println("100's Case: " + matchFruit(100)) } } |
In Code Listing 81, we use match/case to perform a task much like Java's switch/case mechanism. The function matchFruit takes an integer parameter called index, and we match this parameter to various fruits. The first case to correctly match the variable will provide the value to which the variable is mapped. When we call the function with matchFruit(2), it will return "Banana". Likewise, matchFruit(3) returns the string "Kumquat".
If we pass a value that does not match any previous case, the underscore case "_" will execute, and the program will return "Unknown". The underscore character stands for a wild card, just as it does when we import items using the _. We see the output of this program in Code Listing 82.
Code Listing 82: Output from Code Listing 81
2's Case: Banana 3's Case: Cumquat 100's Case: Unknown |
We can also use match/case without defining a separate function. In Code Listing 81, we defined a separate function called matchFruit, but Code Listing 83 shows how to use a match/case to set a variable without calling a distinct function.
Code Listing 83: Simple Matching Example 2
object MainObject { def main(args: Array[String]): Unit = {
// Define some variable var fruitIndex = 2
// Perform the matching var output = fruitIndex match { case 1 => "Apple" case 2 => "Banana" case 3 => "Cumquat" case _ => "Unknown" }
// Output the result: println(fruitIndex + "'s Case: " + output) } } |
We can use the OR operator | and combine several conditions into each case. The example in Code Listing 84 takes an input Int from 1 to 13 and returns the card classification Ace, King, Small, Medium, etc. Notice the use of | to combine several conditions.
Code Listing 84: Combining Conditions with |
object MainObject { def main(args: Array[String]): Unit = {
def classifyPip(x: Int): String = x match { case 1 => "Ace" case 2|3|4 => "Small" case 5|6|7 => "Medium" case 8|9|10 => "Large" case 11 => "Jack" case 12 => "Queen" case 13 => "King" }
println("Pip 5 returns: " + classifyPip(5)) println("Pip 11 returns: " + classifyPip(11)) println("Pip 1 returns: " + classifyPip(1)) } } |
The variables we use in the cases are not the same as any outside variables, even when they have the same names. For instance, Code Listing 85 shows a rather strange output. Study the listing for a moment and try to decide what it will output.
Code Listing 85: Variables in Case vs. Outside
object MainObject { def main(args: Array[String]): Unit = {
// Define some variables val My_Amazing_Variable = "123" val someOtherVar = "456"
// Perform matching: "123" match { case someOtherVar => println("someOtherVar") case My_Amazing_Variable => println("My_Amazing_Variable") } } } |
Looking at Code Listing 85, we might assume the string “123” matches the variable called “My_Amazing_Variable” because that variable is set to “123”. Therefore, we might expect the program in this example to output “My_Amazing_Variable”. But this is not what happens. The program will output “someOtherVar”, and it is important that we know why.
Scala will take the string “123” to match against its cases. The first case is “someOtherVar”. There is a local variable called someOtherVar, but the someOtherVar in the cases is actually shadowing it! The someOtherVar in the cases is not related to the local variable with the same name. “123” definitely matches some random variable name, which means the program will print “someOtherVar” to the screen. It is not testing the value of the local variable someOtherVar, but rather it is assigning “123” to a new variable with the same name. This output would be exactly the same as if we named the first case anyRandomVariable, and the fact that the variable outside the cases shares the same name as the case's variable is irrelevant.
We can test the actual values of local variables in our cases. If we want to use the actual values from the variables defined outside the cases, we must delimit the variable names with back quotes—see Code Listing 86.
Code Listing 86: Delimiting Variable Names with Back Quotes
object MainObject { def main(args: Array[String]): Unit = { // Define some variables: val My_Amazing_Variable = "123" val someOtherVar = "456"
"123" match {
// Use back quotes to test the value of the local // variables: case `someOtherVar` => println("someOtherVar") case `My_Amazing_Variable` => println("My_Amazing_Variable") } } } |
Code Listing 86 will check the values of the local variables called “someOtherVar” and “My_Amazing_Variable”, and it will print “My_Amazing_Variable” to the screen because the string “123” matches the value of this variable as defined outside the scope of the cases.
Match/case in Scala is much more powerful than Java's switch/case. We can match objects as well as simple data types. Code Listing 87 shows an example of matching objects. These examples are all about musical key signatures. The exact meaning of the key names and sharps or flats is irrelevant—the listings are simply illustrations of how matching works.
Code Listing 87: Matching Objects of a Case Class
object MainObject {
// Define a class marked with 'case' modifier case class KeySignature(name: String, sharpsFlats: Int)
def main(args: Array[String]): Unit = { // Define some KeySignature variables: var key1 = new KeySignature("C", 0) var key2 = new KeySignature("Bb", -2) var key3 = new KeySignature("c", -3) // Perform a loop to match our keys: for(key <- List(key1, key2, key3)) {
// Perform the match for each key: val fullKeyName = key match { case KeySignature("C", 0) => "C Major" case KeySignature("Bb", -2) => "B Flat Major" case KeySignature("c", -3) => "C Minor" }
println("Key: " + key + " -> " + fullKeyName) } } } |
In Code Listing 87, we define a class called KeySignature. Note that the class is marked with the modifier case. This is important if we wish to use the class in a match/case. When we mark a class with the case modifier, Scala writes additional methods that enable it to perform pattern matching.
Case classes have an equals method, toString method, a hashcode method, and several other methods written for them. Case classes can be instantiated without the "new" operator because they implement the apply method, and all parameters to the constructor of a case class are public and val. This is important because it allows matching. Without the case modifier, we would need to write our own code to mimic the code in Code Listing 87.
Code Listing 87 shows very basic matching. We can also use the wild card symbol for one or all of the parameters for the cases. This is where the term “pattern matching” really becomes applicable. We are not necessarily matching objects against their exact copies, as we do with a Java switch/case, but instead we are matching objects against patterns.
Code Listing 88 shows an example of using the _ wild card in the determination of key signatures.
Code Listing 88: Using _ as a Wild Card in Cases
object MainObject { case class KeySignature(name: String, sharpsFlats: Int)
def main(args: Array[String]): Unit = { // Define some keys var key1 = new KeySignature("C", 0) var key2 = new KeySignature("Bb", -2) var key3 = new KeySignature("c", -3)
// Loop through the keys, this loop has an additional // couple of keys, "D" and "QWERTY" at the end: for(key <- List(key1, key2, key3, KeySignature("D", 123),// D does not actually have 123 sharps! KeySignature("QWERTY", 5)// 5 sharps is not called QWERTY! )) { // Perform the matching: val fullKeyName = key match { case KeySignature("C", 0) => "C Major" case KeySignature("Bb", -2) => "B Flat Major" case KeySignature("c", -3) => "C Minor"
// Using wild cards for parameters: case KeySignature(_, 5) => "B Major" // B Major has 5 sharps case KeySignature("D", _) => "D Major" // D Major }
println("Key: " + key + " -> " + fullKeyName) } } } |
Code Listing 88 shows that we can match objects even when we do not necessarily match all parameters. The wild card symbol is used in Code Listing 88 to return "B Major" when the key has 5 sharps, and the key name is irrelevant because of the _. Likewise, we can match the key "D Major" by stating that if the key name is "D", then the number of sharps is irrelevant. This is not actually how musical keys work (D Major has two sharps in reality), but this works as an illustration.
We can often use Any as a data type to mean multiple types are returned. Notice how the keyword is used in Code Listing 89 to mean “any data type.”
Code Listing 89: Using Any as a Data Type with Matching
object MainObject { def main(args: Array[String]): Unit = {
// This function takes a single parameter // of any data type: def toColorString(q: Any): Any = q match {
case 1 => "Red" case "1" => "Red" case "one" => "Red" case 2 => "Green" case "2" => "Green" case "two" => "Green" case 3 => "Blue" case "3" => "Blue" case "three" => "Blue"
case _ => -1 }
// Test the matching with some calls to toColorString: println("Color matched for \"one\": " + toColorString("one")) println("Color matched for \"2\": " + toColorString("2")) println("Color matched for 3: " + toColorString(3)) println("Color matched for \"Hello\": " + toColorString("Hello")) } } |
In Code Listing 89, we specify the data type of the function toColorString as Any. This means any data type can be passed as the parameter q. Then we specify that the function returns Any as a data type. This means we can return multiple different data types from this function.
When we match the q variable, we provide cases for Int and String. We also provide a final case that has a pattern of _, the wild card. If none of the previous cases matches, we return -1 as an Int. This is a function that takes multiple parameter types, tests them with a series of cases, and returns either a String or an Int, depending on whether or not the q parameter was matched. If you are familiar with Java programming, this function will look extremely odd.
The next example program uses Any as a data type again. This time, we return a String version of the input if it is an Int, and an Int version if it is a String. Without some context, this is a pointless activity, but it does illustrate how we can easily test and change data types without using the complex syntax that Java requires in order to do the same thing.
Code Listing 90: Flipping Data Types
object MainObject { def main(args: Array[String]): Unit = {
// Define the function to flip data types: def flipStringAndInt(x: Any): Any = x match { case y: Int => y.toString case y: String => y.toInt case _ => "Unknown data type!" }
// Make some test cases: val myInt = flipStringAndInt("190") val myString = flipStringAndInt(190) val unknown = flipStringAndInt(190.0)
// Output results: println("myInt: " + myInt) println("myString: " + myString) println("unknown: " + unknown) } } |
In order to match the type of the argument in a case, we specify another variable—y in the example. We say that y: Int =>, which means the data type of y is Int, then we supply the return value. So, when the data type of y is an Int, the pattern-matching mechanism maps it to a String, and vice versa—String is mapped to Int.
We should note that in Code Listing 90 the function flipStringAndInt returns a String for any input that is an Int, and vice versa. When we pass a Double as the input, the function returns the string Unknown data type!.