Scala: Pattern matching
Today I want to write about a dream of every Java developer – pattern matching. While a switch statement works only with limited number of types, including primitive values, strings and enums, the pattern matching feels confident with almost any argument type. I’m going to demonstrate a batch of examples where pattern matching is useful.
There are a lot of cases when you need to make a decision in a program based on some value. The first thought which comes to mind is IF ELSE operator or Java SWITCH statement. But Scala offers more powerful and elegant approach for handling such scenarios. Let’s consider how pattern matching can help you.
Here is a simple example of pattern matching:
1 + 2 - 3 + 4 - 5 match { case -1 => "minus one" case 0 => "zero" case 1 => "one" case _ => "unknown" } //minus one
In the first line we define an algebraic expression, then we use a special keyword match
. After that follows curly bracket. In lines 2, 3, 4 and 5 we put possible patterns. They represent cases which we expect to get as a result of the expression evaluation. Also pay extra attention to the underscore symbol. It means everything else or default pattern. Finally curly bracket is closed.
Summing up here is a general syntax for a pattern matching:
We can match
any expression, then in case
sections we describe what we expect to get. If the expression matches some particular case, pattern matching stops and returns expression which corresponds to the matched case. If there is no any match, then MatchError
exception is thrown. That’s why, as a rule, in the last case
line developers put underscore (I mentioned it before), it serves as default expression.
Pattern matching examples
Let’s examine several samples where pattern matching is used. The first example is the most simple. We are going to match expression to constants.
val boo: Boolean = 5 < 10 boo match { case true => 1 case false => 0 } //1
In the next example we put a pattern matching in a function and use underscore in case when all previous patterns are not matched.
def matchFunction(v: Int) = v match { case 1 => "one" case 2 => "two" case _ => "unknown number" } matchFunction(2) //two matchFunction(5) //unknown number
Well, now we can continue with more complex samples. Let’s apply pattern matching to case classes:
trait Payment { def pay(amount: Double): Unit } class Cash extends Payment { def pay(amount: Double): Unit = println(s"Pay with cash $amount") } case class CreditCard(fullName: String) extends Payment { def pay(amount: Double): Unit = println(s"Pay with credit card $amount") def verify(): Unit = println("Verification...") } def processPayment(amount: Double, method: Payment) = method match { case cash: Cash => cash.pay(amount) case card: CreditCard => { card.verify() card.pay(amount) } case _ => println("Unknown payment method") } val paymentA = new Cash val paymentB = new CreditCard("Alex Zvolinskiy") processPayment(10, paymentA) //Pay with cash 10.0 processPayment(50, paymentB) //Verification... //Pay with credit card 50.0
This time we matched patterns and then used them. In order to do this you can simply use following construction:
... case aliasName: SomeType => aliasName.particularMethod() ...
There is one more way to write pattern matching expressions:
trait Alcohol case class Beer(price: Double, name: String) extends Alcohol case class Wine(price: Double, name: String, year: Int) extends Alcohol def buyAlcohol(alco: Alcohol) = alco match { case Beer(price, name) => println(s"You pay $price for $name") case Wine(price, name, year) => println(s"You pay $price for $name ($year)") }
As you see now we match patterns by type parameters: Beer(price, name)
and Wine(price, name, year)
. This approach is helpful when you want to operate exactly with the pattern arguments.
Based on these examples we can cover most part of the real life scenarios. Furthermore pattern matching can be nested. But as in case with IF-ELSE when level of nesting exceeds 2, code become hard to read.
Collections & pattern matching
What is really cool is that you can apply pattern matching even to collections. It’s very convenient to deal with collection elements using pattern matching. Here is basic example:
val commonList = List(1, 2, 3, 4, 5) val emptyList = Nil val oneElement = 'a' :: Nil def checkList[T](list: List[T]): String = list match { case Nil => "Empty list" case list :: Nil => "One element" case _ => "More than one element" } checkList(emptyList) //Empty list checkList(oneElement) //One element checkList(commonList) //More than one element
Here is how you can use pattern matching in a recursive function for summing of integers.
val commonList = List(1, 2, 3, 4, 5) def sum(list: List[Int]): Int = { def recursion(sum: Int, list: List[Int]): Int = list match { case Nil => sum case el :: tail => (recursion(sum + el, tail)) } recursion(0, list) } sum(commonList) //15
For-expressions & pattern matching
I bet you thought that all possible cases where pattern matching could be used are finished. But I still have a couple of them. So let’s consider a for-expression:
case class Car(pureWeight: Int, equipment: Int) val cars = Seq(Car(1600, 250), Car(1900, 140), Car(1650, 200)) for (Car(weight, eqWeight) <- cars) yield { weight + eqWeight } //List(1850, 2040, 1850)
Here we calculated total weight of cars with equipment. Then we can use these values for example in some checkWeightRestriction(weight: Int)
function.
Finally I want to show the most smallest pattern matching example:
val pair = ("Alex", "Texas") pair match {case (name, city) => s"Welcome $name from $city"}
We can disintegrate almost everything in a pattern and then use it in a convenient way for us. That’s why pattern matching so useful in Scala. It’s very handy to operate with values without casting them using instanceOf
etc. Also it’s much more readable than IF-ELSE chains.
I described the most crucial and popular examples where a pattern matching applies. I’m sure that I missed some more rare scenarios of pattern matching, so I’m waiting for these examples in comments. You are welcome to write your opinion!