This section will look at scala’s powerful pattern matching mechanism, and the role of extractors in pattern matching. and extractors.

Pattern Matching

Is an expression of form

expr match { 
  case pattern-1 => expr-1 
    ... 
  case pattern-n  => expr-n 
}

First Matching expression is always returned and if no match occurs then MatchError is thrown. E.g suppose we have traits and case classes like

trait Expr 
case class Num(x:Int) extends Expr 
case class Sum(e1:Expr, e2:Expr) extends Expr

Now we can write an evaluation function using pattern matching in following way

def eval(e: Expr):Int = e match { 
  case Num(x) => x 
  case Sum(e1, e2) => eval(e1) + eval(e2) 
}
eval(Sum(Num(1), Sum(Num(5), Num(7)))) //Gives result as 13

 

Pattern construction

Patterns are constructed from

  1. Constructors . e.g Number(x), which internally calls Number.apply(x)
  2. Variables. e.g e1, e2.
    1. Variables names cannot repeat in a pattern. E.g Sum(x,x) is an invalid pattern.
    2. Variable names must begin with a lowercase letter.
  3. Constants. e.g 1, true, N, etc
    1. Names of constants begin with a capital letter, with the exception of the reserved words null , true , false.
  4. Wildcard patterns. e.g Num(_) or Sum(e1, _)

Note: Wildcard can also form the else part of pattern matching. e.g

e match { 
  case p1 => e1 
  case p2 => e2 
  case _ => e3 //else part 
}

Pattern Guards

Pattern guards are simply boolean expressions which are used to make cases more specific. Just add if <boolean expression> after the pattern. e.g

trait Recipient 
case class Email(email:String) extends Recipient 
case class SMS(phoneNumber: String) extends Recipient
val blackListEmail: List[String] //black list of emails
def sendMessage(target: Recipient):Unit = target match { 
  case Email(emailAddress) 
    if (!blackListEmail.contains(emailAddress)) => sendMail(emailAddress) 
  case SMS(phone) => sendSMS(phone) 
  case _ => println("Cannot send mail to to target") 
}

Matching On Types (equivalent to Instance Of)

trait Device  
case class Phone extends Device { 
  def screenOff() 
} 
case class Laptop extends Device {
  def turnScreenSaverOn() 
}
def screenoff(device:Device) = device match { 
  case p: Phone => p.screenOff()
  case l: Laptop => l.turnScreenSaverOn() 
}

 

 

 

 

Extractors

What are extractors

  1. An extractor has the opposite role of a constructor
  2. While a constructor creates an object from a given list of parameters, an extractor extracts the parameters from which an object passed to it was created.
  3. Extractors are automatically available in the companion objects for case classes, however for normal classes we have to implement unapply() for it to be used as an extractor.
  4. Scala calls an extractor’s unapply() method if the extractor is used as an extractor pattern.

 

Types of unapply()

Extracting Single Value

def unapply(object: S): Option[T]
  1. The method expects some object of type S and returns an Option of type T, which is the type of the parameter it extracts
  2. If the result of calling unapply()
    1. If it is Some[T], this means that the pattern matches, and the extracted value is
      bound to the variable declared in the pattern.
    2. If it is None, this means that the pattern doesn’t match and the next case
      statement is tested

Extracting Multiple Values

def unapply(object: S): Option[(T1, ..., Tn)]

The method expects some object of type S and returns an Option of type TupleN, where
N is the number of parameters to extract.

Extracting Boolean

def unapply(object: S): Boolean

expects a value of type S and returns a Boolean.
Used in a pattern, the pattern will match if the extractor returns true. Otherwise the next
case, if available, is tried.

 

 

Example: Extractor with and without case classes

In below Example extractors work automatically for case classes TypeOne / TypeTwo /TypeThree. However for normal classes TypeFour/TypeFive, we had to explicitly create a companion object with unapply() method, for it to be used as an extractor in pattern matching.
Note: The companion objects that we create could be of any name. (see TypeFive example below).

trait Types

case class TypeOne(x: String) extends Types
case class TypeTwo(x: String) extends Types
case class TypeThree(x: String, y: Int) extends Types

class TypeFour(val x: String) extends Types
class TypeFive(val x: String, val y:Int) extends Types

object TypeFour{
  def unapply(arg: TypeFour): Option[String] = Some(arg.x)
}

object FiveType {
  def unapply(arg: TypeFive): Option[(String, Int)] = 
    Some((arg.x, arg.y))
}

 

def performAction(input: Types) = input match {
  case TypeOne(x) => println(s"Type One found = ${x}")
  case TypeTwo(x) => println(s"Type Two found = ${x}")
  case TypeThree(x,y ) => println(s"Type Three found = ${x}, ${y}")
  case TypeFour(x) => println(s"Type Four found = ${x}")
  case FiveType(x,y ) => println(s"Type Five found = ${x}, ${y}")
  case _ => println("Invalid Type")
}

 

val t1 = TypeOne("dummy")
val t2 = TypeTwo("dummy")
val t3 = TypeThree("dummy", 10)
val t4 = new TypeFour("dummy")
val t5 = new TypeFive("dummy", 20)

performAction(t1) //Type One found = dummy
performAction(t2) //Type Two found = dummy
performAction(t3) //Type Three found = dummy, 10
performAction(t4) //Type Four found = dummy
performAction(t5) //Type Five found = dummy, 20

 

 

Extractors on Streams

We can de-structure lists and streams in a way that is akin to one of the ways we can create them, using the cons operator, :: or #::, respectively
Below is example of extractor on Streams.

def sumStream(l: Stream[Int]):Int = l match {
  case x#::tail => x + sumStream(tail)
  case _ => 0
}

sumStream((5 to 8).toStream)
sumStream((1 to 1).toStream)
sumStream(Stream.empty)

 

The above is only possible because extractors can be written in two ways.
E.g. for an extractor where, e is the extractor and p1 and p2 are the parameters to be extracted from a given data , we can write extractors as
o Extractor Pattern notation – e(p1, p2)
o Infix Notation – p1 e p2
The Extractor pattern notation was used in the case classes, whereas the infix notation pattern is used for list/streams, etc. So in the above example of list, we can write the pattern in either of the following way – head#::tail OR #::(head, tail)

 

References

http://danielwestheide.com/blog/2012/11/21/the-neophytes-guide-to-scala-part-1-extractors.html

https://www.coursera.org/learn/progfun1/home/welcome