Wednesday, October 31, 2012

Scala Pattern Matching & Decomposition

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
I would like introduce val types, tuples, singleton object, case class to help us with the examples for each variant.

// 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 pattern
Certain 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"
    
    }
Let us invoke the matchStableIdentifiers function
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 function
scala> 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 case
scala> 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 sequences
scala> 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 exceptions
The 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 values
scala> 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")