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