Pattern Matching

8 minute read

Pattern matching is a powerful tool in Scala. it has a common syntax like

v match {
    case v1 if ??? => ???
    case v2 => ???
    case _ => ???
}

In this chapter we will discuss how to use it in different scenarios

How to match literal value?

Let’s say we have an int variable v:Int , we want to implement the following logic

  • if v==1 then print This is 1
  • if v==2 then print This is 2
  • for the others just print I don't care about this number

Usually, we can do it by if-else

if(v==1)
    println("This is 1")
else if (v==2)
    println("This is 2")
else
    println("I don't care about this number")

We also can do it by pattern-matching

v match {
    case 1 => println("This is 1")
    case 2 => println("This is 2")
    case _ => println("I don't care about this number")
}

In this block we just give the value to each branch directly and use _ to cover the rest of values. _ is a powerful symbol in scala, it has different meanings in different scenario, we will talk about it detailedly in other chapter. In this block, it stand for any value of any type.

Pattern matching also support the if-guard, the previous example can also be implemented like this

v match {
    case _ if v == 1 => println("This is 1")
    case _ if v == 2 => println("This is 2")
    case _ => println("I don't care about this number")
}

The meaning of case _ if v == 1is if any value of any type equals to 1 then do something.

There is also an OR logic in pattern matching, try following example

v match {
    case 1|2 => println("This is 1 or 2")
    case _ => println("I don't care about this number")
}

How to match variable?

This scenario is an edge case, but it’s very interesting. Let’s say we have a function like this

def dosomething(v:Int,given:Int):Unit ={
    .....
}

We want this function to print a log got itwhen v==given, how do we implement it using pattern matching?

Could we do it like this?

def dosomething(v:Int, given:Int):Unit ={
    v match {
        case given => println("got it")
        case _ => println("no idea")
    }
}

When we run this code, we will find this function print got itfor any given parameter, Why?

The meaning of case given => println("got it")here is assign any values of any type to given then do something.

The compiler treat given as a new variable which contains the value of v, not the value of parameter given. To make compiler do the right thing, we need to use ```given ` like this

def dosomething(v:Int, given:Int):Unit ={
    v match {
        case `given` => println("got it")
        case _ => println("no idea")
    }
}

How to match type?

Somethimes we want to do differnt thing for different type, for example

  • If the type is Int, then print This is Int
  • If the type is String, then print This is String
  • For other types, just print no idea

We can implement it using pattern matching like this

def dosomething(v:Any)={
    v match {
        case _:Int => println("This is Int")
        case _:String => println("This is String")
        case _ => println("no idea")
    }
}

We see case _ agagin ! the last one has the same meaning as previous examples. for case _:Int=>, it means do something for any values of Int type.

It’s pretty simple, right? How about change Any to List[Any]? does this code work well?

def dosomething(v:List[Any])={
    v match {
        case _:List[Int] => println("This is Int")
        case _:List[String] => println("This is String")
        case _ => println("no idea")
    }
}

Opps, we get a wrong answer!

$ dosomething(List("A","B","C"))
This is Int

Actually when we compile this code, we will get a warning

Warning:(43, 19) non-variable type argument String in type pattern List[String] (the underlying of List[String]) is unchecked since it is eliminated by erasure
          case _: List[String] => "String"

Scala is a JVM language, the Scala code will be translated to Java code. The type patameter in Java Generics will be erased. So for higher kind type in Scala, we can’t match the inner type.

How to match case class?

case class plays an important role in Scala, it give us a bunch of utility methods and sugar syntaxs. we will have a look at how it works in pattern matcing.

Unlike the common class, we can create a case class instance by class name

case class MyClass(id:Int)
val myClass = MyClass(1)

In pattern matching, we also can get the content of case class by class name

def extractId(v:Any)={
    v match {
        case MyClass(id) => println(s"The id is ${id}")
        case _ => println("No idea")
    }
}

In this example, case MyClass(id)=> will do three things

  • check if the type of v is MyClass
  • assign the id of MyClass to variable id
  • do some logic

We must give the same number of variable as the number of constructor parameter, or we will get a compile error.

We also can apply the type matching and if-guard on the case class matching

case class MyClass(info:Any)
def extractIntInfo(v:MyClass) = {
    v match {
        case MyClass(id:Int) if id > 0 => println(s"The id is ${id}")
        case _ => println("No idea")
    }
}
$ extractIntInfo(MyClass(1))
The id is 1
$ extractIntInfo(MyClass(-1))
No idea
$ extractIntInfo(MyClass("hello"))
No idea

Usually we will use nested case class, how do we extract the information of nested case class? Let’s see a example

case class Name(name:String)
case class Age(age:Int)
case class Address(address:String)
case class Person(name:Name,age:Age,address:Address)
def getAddress(p:Person):String = {
    ???
}

We can implement getAddress like this

def getAddress(p:Person):String = {
    p match {
        case Persion(_,_,Address(a)) => println(s"The address is ${a}")
        case _ => println("No idea")
    }
}

You may noticed we use _ for name and age value, because we don’t care about these two value. for Address, we just use the non-nested case class matching directly.

What if we want to get Addressand Address.address together? Try the following example

def getAddress(p:Person):String = {
    p match {
        case Persion(_,_,v@Address(a)) =>
        	println(s"The Address is ${v}, Address.address is ${a}")
        case _ => println("No idea")
    }
}

We can bind the nested pattern to a variable using @

Beside the previous examples, we may get a case class like this

case class MyClass(info:String*)

info:String*is a variable argument list, its length is not fixed, how should we match it?

Let’t try this example

def getInfo(v:MyClass):List[String] = {
    v match {
        case MyClass(x@_*) => x.toList
        case _ => List()
    }
}
$ getInfo(MyClass("hello","world"))
res2: List[String] = List("hello", "world")
$ getInfo(MyClass())
res3: List[String] = List()

x@_* will assign the variable parameter list to x

How to match collection?

In this part, we will talk about two topics: Tuple and List

Tuple

Tuple is simple, we can just treat it as a special case class which has no class name. we can do pattern matching like this

def extractInfo(v:(Any,Any)) = {
    v match {
        case (k:Int,v:String) => println(s"Key:${k},Value:${v}")
        case _ => println("No idea")
    }
}
$ extractInfo((1,"ok"))
Key:1,Value:ok
$ extractInfo((1,2))
No idea
$ extractInfo(("hello","world"))
No idea

List

Scala List is very differnt from the List in Java, it’s a recursion data structure, let’s see how can we play it with pattern matching.

We can get the head of List

def getHead(l:List[Int]) = {
    l match{
        case head::tail => println(s"The head is ${head}")
        case _ => println("No idea")
    }
}
$ getHead(List(1,2,3))
The head is 1
$ getHead(List())
No idea

We also can iterate every element in List

def iterate(l:List[Int]):Unit = {
    l match{
        case head::tail => 
          println(head)
          iterate(tail)
        case _ => 
          println("end")
    }
}
$ iterate(List(1,2,3))
1
2
3
end
$ iterate(List())
end

If we know the exact length of List, we also can get all the elements at the same time

def getThreeElements(l:List[Int]) = {
    l match {
        case List(e1,e2,e3) => 
        	println(e1)
            println(e2)
            println(e3)
        case _ => println("No idea")
    }
}
$ getThreeElements(List(1,2,3))
1
2
3
$ getThreeElements(List(1,2))
No idea
$ getThreeElements(List())
No idea

In these examples, List has two pattern in pattern matching

  • case head::tail=>

    Iterate the element of List from left to right

  • case List(e1,e2,e3) =>

    Get all the elements of List if the length of List is equal to 3

How to match regex?

Sometimes we need to check if the given string follows some pattern(integer etc.) or extract some information from given string, let’s see how to implement them using pattern matching.

Try this example to check if the input string is an integer

def isInt(s:String) ={
    val intRegex = "\\d*".r
    s match {
        case intRegex() => println(s"${s} is an integer")
        case _ => println(s"${s} is not an integer")
    }
}
$ isInt("1")
1 is an integer
$ isInt("hello")
hello is not an integer

Try this example to extract the name for string Name=bob

def getName(s:String) = {
    val nameRegex = "Name=(.*)".r
    s match {
        case nameRegex(n) => println(s"The name is ${n}")
        case _ => println("No idea")
    }
}
$ getName("Name=bob")
The name is bob
$ getName("Wow!")
No idea

The regex in pattern matching will always try to extract something, so even you don’t have group in your regex expression you also need a () following the regex.

The dark magic of unapply

The source of truth for pattern matching is unapply , the pattern matching is just a sugar syntax of unapply.

For this simple code

v match{
    case MyClass(a) => ???
}

We can rewrite it using unapply

val aOption:Option[A]=MyClass.unapply(v)
if(aOption.nonEmpty){
    val a = aOption.get
    ???
}

The declaration of unapply

  • extract single value

    object MyClass{
        def unapply(v:MyClass):Option[A]
    }
    

    A can be any type

  • extract multiple values

    object MyClass{
        def unapply(v:MyClass):Option[(A1,A2,A3,A4)]
    }
    
  • extract unfixed values(the number of values is not fixed)

    object MyClass{
        def unapplySeq(v:MyClass):Option[Seq[A]]
    }
    

Let’s see some declaration of unapply we used in this chapter, they have been implemented by Scala.

  • case class

    case class MyClass(id:Int)
    object MyClass{
        def unapply(v:MyClass):Option[Int] = Some(v.id)
    }
    
  • List

    object :: {
        def unapply[A](v:List[A]):Option[(A,List[A])] = {
            if(v.isEmpty) None
            else Some(v.head,v.tail)
        }
    }
    
    object List{
        def unapplySeq[A](v:List[A]):Option[Seq[A]] = Some(v)
    }
    

We can define a custom extractor by unapply, it will give us more power.

Try this example, extract the even numbers from List

object Even{
    def unapply(v:List[Int]):Option[List[Int]] = Some(v.filter(_%2==0))
}

def getEven(v:List[Int]):List[Int] = {
    v match {
        case Even(l) => l
        case _ => Nil
    }
}
$ getEven(List(1,2,3,4,5))
res44: List[Int] = List(2, 4)
$ getEven(List())
res45: List[Int] = List()

Comments