Scala Pattern Matching & Decomposition
In this blog post, I will cover various pattern matching variants that I learned in my journey to learn Scala.
- Matching constant values
- Wildcard matcher
- Variable matcher pattern
- Variables and constants (also know as Stable identifier pattern)
- Matching types
- Matching types and restricting the data with guards
- Matching types(tuples and other objects) & decompostion
- Matching lists & decomposing lists
- Matching sequences, ranges and decomposing them
- Pattern matching in catching exceptions
- Define multiple vals with patterns
- Pattern matching in generators
- Decomposing values based on Regular Expressions
// Constants
val TheGood = "The Good"
val TheBad = "The Bad"
val theUgly = "The Ugly"
// A tuple
val tuple = (1, "It is me")
// A singleton object
object Singleton
// A case class
case class Container(x:Any)
// Singleton object with constants
object Constants {
val pi = "3.14"
val PI = 3.14
}
Matching constant values:The following function matches values if x is 3.14 or “3.14”. For all other values it raises scala.MatchError exception indicating that, the pattern matching couldn’t fall in any case listed.
def matchConstant(x:Any) = x match {
case 3.14 => "PI"
case "3.14" => "PI value as String"
}
If you invoke the above function with various values as below:scala> matchConstant(3.14)
res1: String = PI
scala> matchConstant("3.14")
res2: String = PI value as String
scala> matchConstant(2)
scala.MatchError: 2 (of class java.lang.Integer)
Wildcard matcher:We got scala.MatchError as there is no case, that could match the input integer 2. We can fix this by using wildcard matcher pattern so that we can prevent the MatchError.
Added wildcard mather to the matchConstant function:
def matchConstant(x:Any) = x match {
case 3.14 => "PI"
case "3.14" => "PI value as String"
// Wildcard matcher
case _ => "Unknown"
}
Now invocation of matchConstant with integer parameter 2 results as
“Unknown”scala> matchConstant(2)
res5: String = Unknown
Variable matcher patternCertain times you may want to match any value you pass-in. It can be achieved using wildcard. Another alternate way to match is variable pattern. The following example illustrate the variable pattern
def matchAnything(x:Any) = x match {
// Variable pattern
case n => s"Matched value $n"
}
Invoking the above function:scala> matchAnything(1)
res40: String = Matched value 1
scala> matchAnything("1")
res41: String = Matched value 1
scala> matchAnything(Singleton)
res42: String = Matched value Singleton$@1d20b21
Variables and constants (also know as Stable identifier pattern)
The val types identifers and singletons fall under this category. Scala language enforces few rules to match stable identifiers.
- The identifier for constants must start with a capital letter. For example, TheGood and TheBad adhere to this rule.
- If the constant identfier didn’t start with a capital letter, we need to use
backtick(
) . For example.,theUgly` -
If the constant identifer didnt' start with a capital letter and it qualified
the pattern matching will work. For example, Constants.pi is qualified
import Constants._
def matchStableIdentifiers(x:Any) = x match {
}case TheGood=> "The Good" case TheBad=> "The Bad" case `theUgly` => "The ugly" // case `pi` => s"Matched with backticks pi value $x" case Constants.pi => s"Constants.pi has value $x" case PI => s"PI value $x" case Singleton => "The singleton" // Wildcard pattern case _ => s"Any value $x"
scala> matchStableIdentifiers("The Good")
res19: String = The Good
scala> matchStableIdentifiers("The Bad")
res20: String = The Bad
scala> matchStableIdentifiers("The Ugly")
res21: String = The ugly
scala> matchStableIdentifiers(3.14)
res22: String = PI value 3.14
scala> matchStableIdentifiers("3.14")
res23: String = Constants.pi has value 3.14
scala> matchStableIdentifiers(Singleton)
res24: String = The singleton
scala> matchStableIdentifiers("No where to go!")
res25: String = Any value No where to go!
Matching types:The pattern matching can be applied to based on type of the object. This is similar to instanceof operator in Java.
In the following example we match x to case b:Boolean if x hold a boolean type value. Similarly if x holds a value of double type then case d:Double =>… will be matched.
def matchByType(x: Any) = x match {
case b:Boolean => s"A boolean value: $b"
case d:Double => s"An double value: $d"
case _ => "Unknown type"
}
Let us invoke matchByType functionscala> matchByType(true)
res26: String = A boolean value: true
scala> matchByType(false)
res27: String = A boolean value: false
scala> matchByType(1)
res28: String = Unknown type
scala> matchByType(1.0)
res29: String = An double value: 1.0
Matching types and restricting the data with guards:
Let us extends pattern matching to match specific values of a specific type. In the following example we use pattern matching to print a number is even or odd.
def matchValueOfTypeWithGuard(x: Any) = x match {
case x:Int if x % 2 == 0 => s"The input is an even number : $x"
case x:Int if x % 2 != 0 => s"The input is a odd number : $x"
case _ => "Unknown type"
}
Let us invoke the above function with various values:scala> matchValueOfTypeWithGuard(2)
res30: String = The input is an even number : 2
scala> matchValueOfTypeWithGuard(3)
res31: String = The input is a odd number : 3
scala> matchValueOfTypeWithGuard("R")
res32: String = Unknown value
Matching types(tuples and other objects) &
decompostion:Let me show examples of another variant in matching types and also decompose them.
Following example, matches a tuple and decomposes it to extract objects used in constructing the tuple.
def matchByTypeAndDecompostion(x : Any) = x match {
// Matches a tuple and extracts objects used in contructing the tuple
case (x, y) => s"A tuple with values $x and $y"
case (x, y, z) => s"A tuple with values $x, $y and $z"
// Contructor decomposition pattern
case Container(x) => s"Container with value: $x"
case _ => "Unknown type"
}
Let us invoke the above function with various values:scala> matchByTypeAndDecompostion((1, (1,2)))
res36: String = A tuple with values 1 and (1,2)
scala> matchByTypeAndDecompostion((1, (1,2), (1,2,3)))
res37: String = A tuple with values 1, (1,2) and (1,2,3)
scala> matchByTypeAndDecompostion(Container(3))
res38: String = Container with value: 3
scala> matchByTypeAndDecompostion(Container("Hello"))
res39: String = Container with value: Hello
Matching lists & decomposing lists:Following examples demonistrate pattern matching List objects and also decomposing the list objects
def matchList(x:Any) = x match {
// matches a List(1,2,3)
case List(1, 2, 3) => "Matched List(1, 2, 3)"
// Matches a list with only one element and extracts the element by decompising the list
case x :: Nil => s"Only one element in the list $x"
// Matching a list with single element and extracting the element, can be achieved using constructor decomposition
//case List(x) => s"Only one element in the list $x"
// Matches a list with head as integer 2
case 2 :: xs => s"A list with head 2 and tail $xs"
// Matches a list that has integer 3 followed by 4 and other elements.
// Decomposes list and extract rest of the list excluding the first two elements
case List(3, 4, xs @ _*) => s"A list with 3, 4 and tail $xs"
// Decomposing a list as head and tail
case x :: xs => s"A list with head $x and tail $xs"
// Matching a empty list or null
case nil => "Empty List"
// Matching a empty list using contructor decomposition??
//case List() => "Empty List"
}
Let us invoke the function to match a specific casescala> matchList(List(1,2,3))
res49: String = Matched List(1, 2, 3)
scala> matchList(List(1))
res50: String = Only one element in the list 1
scala> matchList(List(2,3,4))
res51: String = A list with head 2 and tail List(3, 4)
scala> matchList(List(3,4,5,6,7,8,9))
res52: String = A list with 3, 4 and tail List(5, 6, 7, 8, 9)
scala> matchList(List(4,5,6,7,8,9))
res53: String = A list with head 4 and tail List(5, 6, 7, 8, 9)
scala> matchList(List())
res54: String = Empty List
scala> matchList(null)
res55: String = Empty List
Matching sequences, ranges and decomposing them:Following example illustrates matching sequences, ranges and decompose them.
def matchSequence[E](x: Seq[E]) = x match {
case x +: Nil => s"Only element is head: $x"
case _ :+ x => s"Last element $x"
case nil => "Empty Sequence"
}
Let us invoke the above function with ranges and sequencesscala> matchSequence( 1 to 3)
res56: String = Last element 3
scala> matchSequence( 1 to 3 take 1)
res57: String = Only element is head: 1
scala> matchSequence( 1 to 3 take 0)
res58: String = Empty Sequence
scala> matchSequence(Seq(1,2,3,4))
res64: String = Last element 4
scala> matchSequence(Seq(1))
res65: String = Only element is head: 1
Pattern matching in catching exceptionsThe following example demonistrates catching various exception using pattern matching.
def stringToPositiveIntOnly(s: String) = try {
val i = s.toInt
println(s"Input value $i")
assert(i > 0)
} catch {
case e: NumberFormatException => "Not a number"
case e:java.lang.AssertionError => "Assertion failed"
}
Let us invoke the above function with various valuesscala> stringToPositiveIntOnly(null)
res84: Any = Not a number
scala> stringToPositiveIntOnly("-1")
Input value -1
res85: Any = Assertion failed
scala> stringToPositiveIntOnly("1")
Input value 1
res86: Any = ()
Defining multiple vals with patterns:The following examples illustrate pattern matching and decompose them to multiple vals
scala> val (a, b) = (1, ("Ramesh", "NY"))
a: Int = 1
b: (String, String) = (Ramesh,NY)
Pattern matching in generators:
for ( keyAndValue <- -="-" vector="vector"> "a", 2 -> "b") )
yield keyAndValue._2
for ( (k, v) <- -="-" 1="1" vector="vector"> "a", 2 -> "b") )
yield v
->->
Decomposing values based on Regular Expressions:
Regular expression can be used to match certain values and decompse the input to extract interesting parts. Notice that the decomposition will be based on the groups matched by regular expression
// Regular Expressions with only one groups
val Yes = "(y|Y)es".r
// Checking regex works
Yes.pattern.matcher("yes").matches
Yes.unapplySeq("yes") will result: Option[List[String]] = Some(List(y))
def catchYes(s:String) = s match {
case Yes(x) => s
case _ => None
}
catchYes("yes")
catchYes("Yes")
// Regular Expressions with multiple groups
val Yes = "(y|Y)(es)".r
// Checking regex works
Yes.pattern.matcher("yes").matches
Yes.unapplySeq("yes") will result: Option[List[String]] = Some(List(y, es))
def catchYes(s:String) = s match {
// As regex has two groups we should have enough parameter to extract
case Yes(x,y) => s
case _ => None
}
catchYes("yes")
catchYes("Yes")