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
- Constructors . e.g Number(x), which internally calls Number.apply(x)
- Variables. e.g e1, e2.
- Variables names cannot repeat in a pattern. E.g Sum(x,x) is an invalid pattern.
- Variable names must begin with a lowercase letter.
- Constants. e.g 1, true, N, etc
- Names of constants begin with a capital letter, with the exception of the reserved words null , true , false.
- 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
- An extractor has the opposite role of a constructor
- 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.
- 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.
- 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]
- The method expects some object of type S and returns an Option of type T, which is the type of the parameter it extracts
- If the result of calling unapply()
- If it is Some[T], this means that the pattern matches, and the extracted value is
bound to the variable declared in the pattern. - If it is None, this means that the pattern doesn’t match and the next case
statement is tested
- If it is Some[T], this means that the pattern matches, and the extracted value is
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
